diff --git a/.github/actions/docker-dryrun/action.yml b/.github/actions/docker-dryrun/action.yml index e9cd88d46f..f5dc7b4e76 100644 --- a/.github/actions/docker-dryrun/action.yml +++ b/.github/actions/docker-dryrun/action.yml @@ -9,10 +9,10 @@ inputs: runs: using: composite steps: - - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Build regular image - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . platforms: ${{ inputs.platform }} @@ -20,7 +20,7 @@ runs: file: Dockerfile cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful - name: Build rootless image - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . platforms: ${{ inputs.platform }} diff --git a/.github/actions/go-cache/action.yml b/.github/actions/go-cache/action.yml index 5abf4e319a..9c7aacd776 100644 --- a/.github/actions/go-cache/action.yml +++ b/.github/actions/go-cache/action.yml @@ -16,34 +16,34 @@ runs: using: composite steps: - if: ${{ github.workflow == 'cache-seeder' }} - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/go/pkg/mod key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} - if: ${{ github.workflow != 'cache-seeder' }} - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/go/pkg/mod key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} restore-keys: gomod-${{ runner.os }}-${{ runner.arch }} - if: ${{ github.workflow == 'cache-seeder' && inputs.lint-cache != 'true' }} - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/go-build key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }} - if: ${{ github.workflow != 'cache-seeder' || inputs.lint-cache == 'true' }} - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 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' && github.workflow == 'cache-seeder' }} - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/golangci-lint key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }} - if: ${{ inputs.lint-cache == 'true' && github.workflow != 'cache-seeder' }} - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/golangci-lint key: golint-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum', '.golangci.yml') }} diff --git a/.github/actions/go-setup/action.yml b/.github/actions/go-setup/action.yml index a99ef0629a..8fd6e519a2 100644 --- a/.github/actions/go-setup/action.yml +++ b/.github/actions/go-setup/action.yml @@ -13,7 +13,7 @@ runs: using: composite steps: - uses: ./.github/actions/free-disk-space - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version-file: go.mod check-latest: true diff --git a/.github/actions/node-setup/action.yml b/.github/actions/node-setup/action.yml index c9ab484d57..5a251cf0e0 100644 --- a/.github/actions/node-setup/action.yml +++ b/.github/actions/node-setup/action.yml @@ -9,14 +9,14 @@ inputs: runs: using: composite steps: - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - if: ${{ inputs.cache == 'true' }} uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 24 + node-version: 26 cache: pnpm cache-dependency-path: pnpm-lock.yaml - if: ${{ inputs.cache != 'true' }} uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 24 + node-version: 26 diff --git a/.github/workflows/cache-seeder.yml b/.github/workflows/cache-seeder.yml index 4e2988adb4..6251578794 100644 --- a/.github/workflows/cache-seeder.yml +++ b/.github/workflows/cache-seeder.yml @@ -29,7 +29,7 @@ jobs: gobuild: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: ./.github/actions/go-setup - run: make deps-backend deps-tools - run: TAGS="bindata" make backend @@ -59,7 +59,7 @@ jobs: include: - { tags: "bindata", target: "lint-backend" } steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: ./.github/actions/go-setup with: lint-cache: "true" diff --git a/.github/workflows/cron-licenses.yml b/.github/workflows/cron-licenses.yml index 2d4e926288..06ea0e3031 100644 --- a/.github/workflows/cron-licenses.yml +++ b/.github/workflows/cron-licenses.yml @@ -12,8 +12,8 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version-file: go.mod check-latest: true diff --git a/.github/workflows/cron-renovate.yml b/.github/workflows/cron-renovate.yml index 843527ba20..83df70b726 100644 --- a/.github/workflows/cron-renovate.yml +++ b/.github/workflows/cron-renovate.yml @@ -20,13 +20,13 @@ jobs: if: github.repository == 'go-gitea/gitea' # prevent running on forks timeout-minutes: 30 steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: renovatebot/github-action@8217b3fc286df088d7c27f3255fe8414463bc0fd # v46.1.15 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - uses: renovatebot/github-action@b50d2ba2bd928235abdcc14d06dfafc217f1c565 # v46.1.18 with: renovate-version: ${{ env.RENOVATE_VERSION }} configurationFile: renovate.json5 token: ${{ secrets.RENOVATE_TOKEN }} env: RENOVATE_BINARY_SOURCE: install # auto-install go/node toolchains needed by post-upgrade tasks. - RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg)$"]' + RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg|generate-codemirror-languages)$"]' RENOVATE_REPOSITORIES: '["go-gitea/gitea"]' diff --git a/.github/workflows/cron-translations.yml b/.github/workflows/cron-translations.yml index 7c215b2c17..936a03eb9c 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3 with: upload_sources: true upload_translations: false diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index 11ff4cec68..4084d82981 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -49,8 +49,8 @@ jobs: e2e: ${{ steps.changes.outputs.e2e }} shell: ${{ steps.changes.outputs.shell }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 id: changes with: filters: | diff --git a/.github/workflows/giteabot-backport.yml b/.github/workflows/giteabot-backport.yml index b90c225b4b..f398d48126 100644 --- a/.github/workflows/giteabot-backport.yml +++ b/.github/workflows/giteabot-backport.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: go-gitea/giteabot@f8a6f4c14d46920b4b5448852be3de72d00066f0 # v1.0.3 + - uses: go-gitea/giteabot@912675d47455ac93be82d8bda4667a02b20a6fe4 # v1.0.4 with: github_token: ${{ secrets.GITEABOT_TOKEN }} gitea_fork: giteabot/gitea diff --git a/.github/workflows/giteabot.yml b/.github/workflows/giteabot.yml index 5ffde2e75e..c439850a20 100644 --- a/.github/workflows/giteabot.yml +++ b/.github/workflows/giteabot.yml @@ -58,7 +58,7 @@ jobs: steps: # pull_request_review runs without repository secrets on fork PRs, so fall # back to the workflow token for the non-backport checks handled here. - - uses: go-gitea/giteabot@f8a6f4c14d46920b4b5448852be3de72d00066f0 # v1.0.3 + - uses: go-gitea/giteabot@912675d47455ac93be82d8bda4667a02b20a6fe4 # v1.0.4 with: github_token: ${{ secrets.GITEABOT_TOKEN || github.token }} checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }} diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index d8129fd5b7..a3e4295f67 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -19,7 +19,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: ./.github/actions/go-setup with: lint-cache: "true" @@ -31,7 +31,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: ./.github/actions/go-setup with: cache: "false" @@ -42,7 +42,7 @@ jobs: - run: make lint-spell - if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' || needs.files-changed.outputs.actions == 'true' - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 with: python-version: 3.14 - if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' @@ -62,7 +62,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: ./.github/actions/go-setup - run: make deps-backend deps-tools - run: make --always-make checks-backend # ensure the "go-licenses" make target runs @@ -72,7 +72,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: ./.github/actions/node-setup - run: make deps-frontend - run: make lint-frontend @@ -85,7 +85,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: ./.github/actions/go-setup - run: make deps-backend generate-go # no frontend build here as backend should be able to build, even without any frontend files diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index a35937f266..50c4ac167c 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -42,7 +42,7 @@ jobs: ports: - "9000:9000" steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: ./.github/actions/go-setup - uses: ./.github/actions/pgsql-shard with: @@ -78,7 +78,7 @@ jobs: ports: - "9000:9000" steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: ./.github/actions/go-setup - uses: ./.github/actions/pgsql-shard with: @@ -90,7 +90,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: ./.github/actions/go-setup - run: make deps-backend - run: make backend @@ -131,7 +131,7 @@ jobs: ports: - "7700:7700" redis: - image: redis:latest@sha256:a505f8b9d8ac3ff7b0848055b4abf1901d6d77606774aa1e38bd37f1197ed2b5 + image: redis:latest@sha256:5d2c689b4b55fc3fab4b0cc8aaa950f85b508c76c1e0f35a90d8f411d55a8b2b options: >- # wait until redis has started --health-cmd "redis-cli ping" --health-interval 5s @@ -151,7 +151,7 @@ jobs: ports: - 10000:10000 steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: ./.github/actions/go-setup - name: Add hosts to /etc/hosts run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts' @@ -208,7 +208,7 @@ jobs: - "587:587" - "993:993" steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: ./.github/actions/go-setup - name: Add hosts to /etc/hosts run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts' @@ -241,7 +241,7 @@ jobs: ports: - 10000:10000 steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: ./.github/actions/go-setup - name: Add hosts to /etc/hosts run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts' diff --git a/.github/workflows/pull-docker-dryrun.yml b/.github/workflows/pull-docker-dryrun.yml index f7483132b5..37ba12e7f5 100644 --- a/.github/workflows/pull-docker-dryrun.yml +++ b/.github/workflows/pull-docker-dryrun.yml @@ -21,7 +21,7 @@ jobs: needs: [files-changed] runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: ./.github/actions/docker-dryrun with: platform: linux/amd64 @@ -31,7 +31,7 @@ jobs: needs: [files-changed] runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: ./.github/actions/docker-dryrun with: platform: linux/arm64 @@ -41,7 +41,7 @@ jobs: needs: [files-changed] runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - 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 bcd5eba381..64d1c4c57d 100644 --- a/.github/workflows/pull-e2e-tests.yml +++ b/.github/workflows/pull-e2e-tests.yml @@ -19,7 +19,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: ./.github/actions/go-setup - uses: ./.github/actions/node-setup - run: make deps-frontend diff --git a/.github/workflows/pull-labeler.yml b/.github/workflows/pull-labeler.yml index dd19055162..8faa840c1c 100644 --- a/.github/workflows/pull-labeler.yml +++ b/.github/workflows/pull-labeler.yml @@ -17,7 +17,7 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0 + - uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0 with: sync-labels: true @@ -30,12 +30,12 @@ jobs: pull-requests: write steps: # Base-branch checkout only: pull_request_target runs with elevated token; never run PR-head code here. - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: ref: ${{ github.event.pull_request.base.sha }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 24 + node-version: 26 # Labels are only synced after the title lints, so an invalid title never reaches the label diff. - run: node ./tools/ci-tools.ts lint-pr-title env: diff --git a/.github/workflows/release-nightly-snapcraft.yml b/.github/workflows/release-nightly-snapcraft.yml index 46ea663f83..456f628c87 100644 --- a/.github/workflows/release-nightly-snapcraft.yml +++ b/.github/workflows/release-nightly-snapcraft.yml @@ -6,36 +6,35 @@ on: - main workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read jobs: build-and-publish: - runs-on: ubuntu-latest - - env: - SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + strategy: + fail-fast: false + matrix: + runner: [ubuntu-24.04, ubuntu-24.04-arm] + runs-on: ${{ matrix.runner }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - - name: Install snapcraft - run: sudo snap install snapcraft --classic - - - name: Remote build + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + fetch-depth: 0 + - uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0 + id: build + # retry snapcraft uploads which can be flaky + - name: Publish snap run: | - snapcraft remote-build \ - --launchpad-accept-public-upload \ - --build-for=amd64,arm64,armhf - - - name: List built snaps - run: find . -maxdepth 1 -type f -name '*.snap' -print - - - name: Upload and release snapcraft nightly build - run: | - set -euo pipefail - - for snap in ./*.snap; do - echo "Uploading $snap to edge" - snapcraft upload --release="latest/edge" "$snap" + for attempt in 1 2 3 4 5; do + snapcraft upload "${{ steps.build.outputs.snap }}" --release latest/edge && exit 0 + echo "::warning::snap upload attempt $attempt failed, retrying in 15s" + sleep 15 done + exit 1 + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 70251bb091..96afd1d2b1 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -13,26 +13,24 @@ jobs: runs-on: namespace-profile-gitea-release-binary permissions: contents: read + id-token: write steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 # 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@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - cache: pnpm - cache-dependency-path: pnpm-lock.yaml + - uses: ./.github/actions/node-setup - run: make deps-frontend deps-backend # xgo build - run: make release env: TAGS: bindata + - name: Install Cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: import gpg key id: import_gpg uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 @@ -45,6 +43,7 @@ jobs: GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }} run: | for f in dist/release/*; do + cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f" done # clean branch name to get the folder name in S3 @@ -57,7 +56,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -75,12 +74,12 @@ jobs: contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 # 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@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - name: Get cleaned branch name id: clean_name env: @@ -88,7 +87,7 @@ jobs: run: | REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//') echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 id: meta with: images: |- @@ -98,7 +97,7 @@ jobs: type=raw,value=${{ steps.clean_name.outputs.branch }} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 id: meta_rootless with: images: |- @@ -112,18 +111,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular docker image - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -133,7 +132,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@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.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 34ed45b281..c59a9a2298 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -14,26 +14,24 @@ jobs: runs-on: namespace-profile-gitea-release-binary permissions: contents: read + id-token: write steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 # 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@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - cache: pnpm - cache-dependency-path: pnpm-lock.yaml + - uses: ./.github/actions/node-setup - run: make deps-frontend deps-backend # xgo build - run: make release env: TAGS: bindata + - name: Install Cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: import gpg key id: import_gpg uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 @@ -46,6 +44,7 @@ jobs: GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }} run: | for f in dist/release/*; do + cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f" done # clean branch name to get the folder name in S3 @@ -58,7 +57,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -86,13 +85,13 @@ jobs: contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 # 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@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 id: meta with: images: |- @@ -105,7 +104,7 @@ jobs: type=semver,pattern={{version}} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 id: meta_rootless with: images: |- @@ -121,18 +120,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular container image - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -140,7 +139,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} annotations: ${{ steps.meta.outputs.annotations }} - name: build rootless container image - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.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 394c524b75..5fce362bc3 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -17,26 +17,24 @@ jobs: permissions: contents: read packages: write # to publish to ghcr.io + id-token: write steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 # 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@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - cache: pnpm - cache-dependency-path: pnpm-lock.yaml + - uses: ./.github/actions/node-setup - run: make deps-frontend deps-backend # xgo build - run: make release env: TAGS: bindata + - name: Install Cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: import gpg key id: import_gpg uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 @@ -49,6 +47,7 @@ jobs: GPG_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }} run: | for f in dist/release/*; do + cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes echo "$GPG_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u "$GPG_FINGERPRINT" --output "$f.asc" "$f" done # clean branch name to get the folder name in S3 @@ -61,7 +60,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 + uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -89,13 +88,13 @@ jobs: contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 # 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@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 + - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 id: meta with: images: |- @@ -112,7 +111,7 @@ jobs: type=semver,pattern={{major}}.{{minor}} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 + - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 id: meta_rootless with: images: |- @@ -133,18 +132,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular container image - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -152,7 +151,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} annotations: ${{ steps.meta.outputs.annotations }} - name: build rootless container image - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 diff --git a/.golangci.yml b/.golangci.yml index b72b3fbebd..519b74f7a8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -107,6 +107,7 @@ linters: - -QF1008 testifylint: disable: + - empty - go-require - require-error usetesting: diff --git a/CHANGELOG.md b/CHANGELOG.md index 691e6eaed1..02e49219a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,359 @@ 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.27.0](https://github.com/go-gitea/gitea/releases/tag/v1.27.0) - 2026-07-13 + +* BREAKING + * Feat(actions)!: improve support for reusable workflows (#37478) + * Use Content-Security-Policy: script nonce (#37232) + +* SECURITY + * Fix: various security fixes (#38406) (#38426) + * Fix(security): harden access checks and migration validation (#38324) (#38400) + * Fix: enforce public-only token scope and harden push options / locale parsing (#38323) (#38399) + * Fix(pull): re-evaluate review official flag on target branch change (#38319) (#38402) + * Fix(api): stop leaking private repo metadata after access revocation (#38321) (#38390) + * Fix(lfs): require proof of possession for cross-repo objects (#38322) (#38389) + * Fix(mirror): disable HTTP redirects on pull mirror sync (#38320) (#38367) + * Fix(release): validate web attachment renames against allowed types (#38314) (#38328) + * Fix(release): gate draft release attachments on web download endpoints (#38318) (#38325) + * Fix(deps): update module github.com/go-git/go-git/v5 to v5.19.1 [security] (#37786) + * Fix(oauth): restrict introspection to the token's client (#38042) + * Fix(api): don't expose private org membership via public_members (#38145) + * Fix(actions): deny fork-PR cross-repo access via collaborative owner (#38214) + * Fix(migrations): prevent path traversal in repository restore (#38215) + * Feat(security): set X-Content-Type-Options: nosniff by default (#37354) + +* FEATURES + * Feat(actions): add workflow status badge modal (#38196) + * Feat(actions): support owner-level and global scoped workflows (#38154) + * Feat(api): support ref suffixes in compare (#38148) + * Feat(actions): implement `jobs..continue-on-error` (#38100) + * Feat(actions): show run status on browser tab favicon (#38071) + * Feat(api): add token introspection and self-deletion endpoint (#37995) + * Feat(repo): split repository creation limit into user and org scopes (#37872) + * Feat(actions): bulk delete, disable and enable runners in admin UI (#37869) + * Feat(actions): List workflows that were executed once but got removed from the default branch (#37835) + * Feat(org): add team visibility so org members can discover teams (#37680) + * Feat: add raw diff/patch endpoint for repository comparisons (#37632) + * Feat(oauth): Support AWS Cognito OAuth2 provider (#37607) + * Feat: Add avatar stacks (#37594) + * Feat(actions): add job summaries (GITHUB_STEP_SUMMARY) (#37500) + * Feat(web): Add Jupyter Notebook (.ipynb) Rendering Support (#37433) + * Support for Custom URI Schemes in OAuth2 Redirect URIs (#37356) + * Feat(orgs): Add search bar for organization members tab page (#37347) + * Feat(api): Add assignees APIs (#37330) + * Feat(api): Add GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs (#37196) + * Introduce `ActionRunAttempt` to represent each execution of a run (#37119) + * Serve OpenAPI 3.0 spec at /openapi.v1.json (#37038) + * Add project column picker to issue and pull request sidebar (#37037) + * Allow multiple projects per issue and pull requests (#36784) + * Add bulk repository deletion for organizations (#36763) + * Add API endpoint to reply to pull request review comments (#36683) + * Feat: Add bypass allowlist for branch protection (#36514) + * Feat(ui): add "follow rename" to file commit history list (#34994) + * Feat(ssh): auto generate additional ssh keys (#33974) + +* ENHANCEMENTS + * Enhance(actions): only create filtered-out workflow commit status for required contexts (#38371) (#38385) + * Enhance: allow builtin default git config options to be overridden (#38172) + * Enhance: allow MathML core elements (#38034) + * Feat(api): add q parameter to list branches API for server-side filtering (#37982) + * Enhance(markup): improve issue title rendering (#37908) + * Enhance(actions): set descriptive browser tab title on run view (#37870) + * Enhance(actions): show workflow name from YAML instead of filename (#37833) + * Feat(actions): add before/after to PR synchronize event payload (#37827) + * Enhance(actions): add branch filters to run list (#37826) + * Enhance(actions): Make Summary UI more beautiful with more infos (#37824) + * Feat: add copy button to action step header, improve other copy buttons (#37744) + * Feat(web): also display PR counts in repo list (#37739) + * Fix(icon): use repo-forked icon to display forks count (#37731) + * Feat(api): add sort and order query parameters to job list endpoints (#37672) + * Feat(api): add last_sync to repository API (#37566) + * Enhance: Adjust Workflow Graph styling (#37497) + * Improve code editor text selection and clean up lint enablement (#37474) + * Add mirror auth updates to repo edit API and settings (#37468) + * Feat: Add default PR branch update style setting (#37410) + * Fix inconsistent disabled styling on logged-out repo header buttons (#37406) + * Allow fast-forward-only merge when signed commits are required (#37335) + * Enhance styling in actions page (#37323) + * Add `ExternalIDClaim` option for OAuth2 OIDC auth source (#37229) + * Fix: improve actions status icons and texts (#37206) + * Make Markdown fenced code block work with more syntaxes (#37154) + * Fix: Sort action run jobs by JobID and Name with matrix examples (#37046) + * Add pagination and search box to org teams list (#37245) + * Workflow Artifact Info Hover (#37100) + * Feat(editor): broaden language detection in web code editor (#37619) + +* PERFORMANCE + * Perf(actions): debounce runner heartbeat writes and throttle task picks (#38281) (#38368) + * Perf(web): sort the action_run query by a repo-scoped index when possible (#38155) + * Perf: Various performance regression fixes (#38078) + * Perf: extend action `c_u` index to include `created_unix` for faster dashboard feeds (#38076) + * Batch-load related data in actions run, job, and task API endpoints (#37032) + +* BUGFIXES + * Fix(util): reject invalid characters between time-estimate units (#38416) (#38423) + * Fix: represent a deleted assignee team as a Ghost team (#38413) (#38419) + * Fix(turnstile): route CAPTCHA verification through the configured proxy (#38412) (#38420) + * Fix: refresh pull request merge box when the commit status is pending (#38410) (#38411) + * Fix: actions task state concurrent update (#38405) (#38409) + * Fix(actions): keep workflow run trailing on one row with long branch names (#38382) (#38403) + * Fix(web): use locale-aware date formatting for contribution calendar tooltips (#38398) (#38401) + * Fix: co-author detection (#38392) (#38397) + * Fix: incorrect co-author detection on commit page (#38386) (#38387) + * Fix(ui): restore commits table column widths (#38379) (#38383) + * Fix: golang html template url escaping (#38363) (#38369) + * Fix: minio init check (#38355) (#38361) + * Fix: org project view assignee list (#38357) (#38360) + * Fix(actions): release claimed task if context is cancelled during `FetchTask` (#38343) (#38347) + * Fix(actions): make runner list pagination order deterministic (#38313) (#38327) + * Fix: Improve since/until when counting commits for X-Total-Count (#38243) (#38304) + * Fix(actions): prevent chevron overlap with log text when timestamps are enabled (#38227) (#38307) + * Fix(workflows): branch protection status checks fail when workflow uses on: paths filter (#38237) (#38302) + * Fix(oauth2): persist linkAccountData during auto-link 2FA flow (#38274) (#38295) + * Fix(actions): allow Actions bot to push to protected branches (#38284) (#38293) + * Fix(actions): include all aggregable run statuses in status filter (#38280) (#38287) + * Fix(archiver): use serializable repo-archive queue payload (#38273) (#38283) + * Fix: update npm dependencies, fix misc issues (#38257) + * Fix(api): respect since/until when counting commits for X-Total-Count (#38204) + * Fix: codemirror regressions (#38248) + * Fix(api): support HEAD requests on all API GET endpoints (#38245) + * Fix(actions): Cleanup workflow status badge code (#38241) + * Fix(web): Correctly align the "disabled" label on larger workflow names (#38240) + * Fix(actions): don't swallow HTML entities into linkified URLs (#38239) + * Fix(packages): accept npm "repository" and "bin" in string form (#38236) + * Fix(actions): fix 500 error when canceling a canceling task (#38223) + * Fix(deps): update module golang.org/x/image to v0.43.0 [security] (#38219) + * Fix(mssql): convert legacy DATETIME columns to DATETIME2 (#38216) + * Fix(api): deny private org member enumeration via /members (#38213) + * Fix(actions): ensure all waiting jobs get runners in large workflows (#38200) + * Fix(deps): update go dependencies (#38194) + * Fix(deps): update npm dependencies (#38193) + * Fix(cli): default must-change-password to false for bot users (#38175) + * Fix(actions): show run index in run view and fix summary graph height (#38165) + * Fix: csp (#38162) + * Fix(deps): update npm dependencies (#38123) + * Fix(mssql): expand legacy issue and comment long-text columns (#38120) + * Fix(packages): validate debian distribution and component names (#38116) + * Fix(packages): validate module version in goproxy ParsePackage (#38104) + * Fix(deps): update dependency esbuild to v0.28.1 [security] (#38097) + * Fix: git push hook post receive (#38089) + * Fix(ui): prevent commit status popup overflowing its row (#38081) + * Fix: validate gem name in rubygems parseMetadataFile (#38061) + * Fix: commit display name (#38057) + * Fix: csp regressions (#38047) + * Fix: api error message (#38031) + * Fix(deps): update npm dependencies (#38029) + * Fix: pgsql lint (#38022) + * Fix(indexer): fix assignee filters in issue search (#38021) + * Fix: various dropdown problems (#38020) + * Fix: refactor git error handling and make archive streaming handle non-existing commit id (#38007) + * Fix: raise git required version to 2.13 (#37996) + * Fix: remove "no-transfrom" from the cache-control header (#37985) + * Fix(deps): update module github.com/google/go-github/v87 to v88 (#37971) + * Fix: use committer time where ever possible as default (#37969) + * Fix(deps): update npm dependencies, remove nolyfill (#37968) + * Fix(deps): update go dependencies (#37967) + * Fix(pull): preserve squash message trailers and additional commit messages (#37954) + * Fix(deps): update module golang.org/x/image to v0.41.0 [security] (#37904) + * Fix: support ##[command] log prefix in action run UI (#37882) + * Fix(deps): update module github.com/google/go-github/v86 to v87 (#37845) + * Fix(deps): update npm dependencies (#37844) + * Fix(deps): update go dependencies (#37841) + * Fix(frontend): resolve Vite assets by manifest source path (#37836) + * Fix(locales): Replace hardcoded strings (#37788) + * Fix(packages): render markdown links relative to linked repo (#37676) + * Fix: persist mirror repository metadata (#37519) + * Fix cmd tests by mocking builtin paths (#37369) + * Add `form-fetch-action` to some forms, fix "fetch action" resp bug (#37305) + * Feat: execute post run cleanup when workflow is cancelled (#37275) + * Fix `relative-time` error and improve global error handler (#37241) + * Refactor flash message and remove SanitizeHTML template func (#37179) + * Fix Repository transferring page (#37277) + +* TESTING + * Test(e2e): fix race in pdf file render test (#38380) (#38381) + * Test: compare key file contents instead of `FileInfo` in `TestInitKeys` (#38330) (#38331) + * Test: speed up two tests (#37905) + * Test: Fix random failure test (#37887) + * Test: fix flaky `issue-comment` close test (#37880) + * Test: enable WAL for sqlite integration tests (#37861) + * Test: fix flaky `TestResourceIndex` and reduce its runtime (#37847) + * Test: run `TestAPIRepoMigrate` offline via a local clone source (#37817) + * Ci: shard tests and reduce redundant work (#37618) + * Test(e2e): run playwright via container (#37300) + * Remove external service dependencies in migration tests (#36866) + * Refactor: only reset a database table when the table's data was changed (#37573) + +* BUILD + * Refactor: use modernc sqlite driver as default (#37562) + * Fix(actions): authenticate snapcraft before nightly remote build (#38252) + * Ci: cap Elasticsearch heap in db-tests (#37816) + * Build(snap): publish nightly version to snapcraft via actions (#37814) + * Ci: split pgsql shards into plain jobs, dedupe setup actions (#37802) + * Ci: narrow files-changed frontend filter (#37749) + * Ci: add `zizmor` to `lint-actions` (#37720) + * Chore: clean up "contrib" dir (#37690) + * Fix: snap build (main branch) (#37685) + * Ci: Also lint json5 files (#37659) + * Build: update pnpm to v11 (#37591) + * Refactor(deps): migrate from `nektos/act` fork to `gitea/runner` (#37557) + * Update go js py dependencies (#37525) + * Ci: lint PR titles with commitlint (#37498) + * Chore: upgrade Go version in devcontainer image to 1.26 (#37374) + * Update GitHub Actions to latest major versions (#37313) + * Update go js dependencies (#37312) + * Fail vite build on rolldown warnings via NODE_ENV=test (#37270) + * Replace custom Go formatter with `golangci-lint fmt` (#37194) + * Integrate renovate bot for all dependency updates (#37050) + * Build(sign): move to sigstore (#38250) + +* DOCS + * Docs: update changelog for 1.26.3 & 1.26.4 (#38178) + * Update 1.26.1 changelog in main (#37442) + * Docs: fix duplicated word in foreachref doc comment (#38161) + * Docs: Clarify criteria for becoming a merger (#38113) + * Docs: Publish TOC Election Result 2026 (#38111) + * Docs: mark openapi3 as autogenerated in attributes (#37963) + * Docs: add development setup guide (#37960) + +* MISC + * Refactor: lint bare `fill`/`stroke` colors, add vars for git graph color series (#37543) + * Remove htmx (#37224) + * Refactor htmx and fetch-action related code (#37186) + * Revert(sign): restore gpg (#38251) + * Refactor: replace legacy `delete-button` with `link-action` (#38143) + * Refactor(actions): read runner capabilities from proto field (#38068) + * Refactor(api): clarify APIError message usage and fix legacy lint error (#38012) + * Refactor: Use db.Get[] instead of db.GetEngine(ctx).Get(bean) to avoid zero value fetching wrong database record (#37977) + * Enhance: Migrate remaining gopkg.in/yaml.v3 usages to go.yaml.in/yaml/v4 (#37866) + * Fix(deps): update go dependencies (#37851) + * Ci: Fix sync PR labels from the conventional-commit title (#37784) (#37825) + * Ci: tweak `files-changed`, add `free-disk-space` (#37819) + * Fix(deps): update module golang.org/x/crypto to v0.52.0 [security] (#37806) + * Test(e2e): add comment, release, star, PR and fork tests (#37800) + * Chore: simplify issue and pull request templates (#37799) + * Chore: Update giteabot to fix failure when backport (#37789) + * Fix(api): handle partial failures in push mirror synchronization gracefully (#37782) + * Fix(deps): update module gitlab.com/gitlab-org/api/client-go/v2 to v2.26.0 (#37771) + * Ci: split giteabot workflow (#37770) + * Fix(deps): update npm dependencies (#37768) + * Refactor(waitgroup): replace Add/Done goroutines with WaitGroup.Go (#37764) + * Fix(deps): update module google.golang.org/grpc to v1.81.1 (#37762) + * Ci: fix cache-related issues (#37761) + * Chore: fix tests (#37760) + * Fix(deps): update module github.com/google/go-github/v85 to v86 (#37754) + * Fix(deps): update npm dependencies (#37753) + * Fix(deps): update go dependencies (#37752) + * Chore(deps): update action dependencies (#37751) + * Fix(markup): wrap indented code blocks for the code-copy button (#37748) + * Chore(db): introduce db.Session and db.EngineMigration interfaces (#37746) + * Refactor(glob): use strings.Builder for regexp compilation (#37730) + * Chore(doctor): remove four obsolete doctor check implementations (#37728) + * Refactor(org): simplify owner-team org repo creation logic (#37727) + * Refactor: move `workflowpattern` into `modules/actions` (#37717) + * Chore: clean up tests (#37715) + * Style: misc UI fixes (#37691) + * Ci: add shellcheck linter (#37682) + * Fix: catch and fix more lint problems (#37674) + * Fix(deps): update dependency mermaid to v11.15.0 [security], add e2e test (#37662) + * Fix(deps): update npm dependencies (#37647) + * Ci(renovate): update Go import paths on major bumps (#37641) + * Fix(deps): update go dependencies (major) (#37639) + * Chore(deps): update action dependencies (major) (#37638) + * Fix(deps): update module code.gitea.io/sdk/gitea to v0.25.0 (#37637) + * Fix(deps): update npm dependencies (#37636) + * Refactor(log): replace log.Critical with log.Error (#37624) + * Build(deps): bump fast-uri from 3.1.0 to 3.1.2 (#37616) + * Chore(deps): update action dependencies (#37603) + * Ci: allow `chore` type in PR title lint (#37575) + * Ci: increase renovate frequency and fix RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS (#37565) + * Docs: fix 4 typos in CHANGELOG.md (#37549) + * Fix(deps): update go dependencies (#37541) + * Chore(deps): update action dependencies (#37540) + * Refactor pull request view (6) (#37522) + * Fix: redirect early CLI console logger to stderr (#37507) + * Refactor "flex-list" to "flex-divided-list" (#37505) + * Refactor compare diff/pull page (1) (#37481) + * Refactor pull request view (4) (#37451) + * Refactor: use named `Permission` field in `Repository` struct instead of anonymous embedding (#37441) + * Replace `olivere/elastic` with REST API client, add OpenSearch support (#37411) + * Refactor: serve site manifest via `/assets/site-manifest.json` endpoint (#37405) + * Remove IsValidExternalURL/IsAPIURL and use IsValidURL at call sites (#37364) + * Update `Block a user` form (#37359) + * Move review request functions to a standalone file (#37358) + * Enable strict TypeScript, add `errorMessage` helper (#37292) + * Refactor frontend `tw-justify-between` layouts to `flex-left-right` (#37291) + * Update Nix flake (#37284) + * Remove `SubmitEvent` polyfill (#37276) + * Remove dead code identified by `deadcode` tool (#37271) + * Upgrade go-git to v5.18.0 (#37268) + * Don't add useless labels which will bother changelog generation (#37267) + * Move heatmap to first-party code (#37262) + * Tests/integration: simplify code (#37249) + * Remove error returns from crypto random helpers and callers (#37240) + * Refactor: simplify ParseCatFileTreeLine and catBatchParseTreeEntries (#37210) + * Refactor "htmx" to "fetch action" (#37208) + * Update go js py dependencies (#37204) + * Add comment for the design of "user activity time" (#37195) + * Remove outdated RunUser logic (#37180) + * Models/fixtures: add "DO NOT add more test data" comment to all yml fixture files (#37150) + * Update javascript dependencies (#37142) + * Update go dependencies (#37141) + * Frontport changelog of v1.26.0-rc0 (#37138) + * Extend issue context popup beyond markdown content (#36908) + +## [1.26.4](https://github.com/go-gitea/gitea/releases/tag/1.26.4) - 2026-06-21 + +* SECURITY + * fix(auth): do not auto-reactivate disabled users on OAuth2 callback (#38009) (#38183) + +* BUGFIXES + * fix: walk git log context error handling (#38182) (#38185) + +## [1.26.3](https://github.com/go-gitea/gitea/releases/tag/1.26.3) - 2026-06-18 + +* BREAKING + * fix(actions)!: require merged PR to bypass fork PR approval gate (#38010) (#38041) + +* SECURITY + * fix(hostmatcher): patch incorrect private list (#38170) (#38173) + * fix: Various security fixes (#38103) (#38151) + * fix: Various sec fixes (#38108) (#38147) + * fix: allow git clone of private repos with anonymous code access (#38074) (#38146) + * fix(auth): ignore stale OIDC external login links to organizations (#37875) (#38141) + * fix(hostmatcher): block reserved IP ranges from external/private filters (#38039) (#38059) + * fix(lfs): require Code-unit access for cross-repo LFS object reuse (#38006) (#38050) + * fix(lfs): reject unknown SSH LFS sub-verbs to prevent auth bypass (#38008) (#38015) + * fix: bound CODEOWNERS regex match time (#38011) (#38025) + * fix: bound debian ParseControlFile to a single control stanza (#38044) (#38055) + * fix(deps): update module golang.org/x/net to v0.55.0 [security] (#37813) (#37829) + +* API + * feat(api): add Link header in ListForks (#38052) (#38063) + +* BUGFIXES + * fix: Fix the panic when ssh remote lfs endpoint parsing failure (#38026) (#38158) + * fix(api): nil pointer panic when filtering tracked times by a non-existent user (#38112) (#38115) + * fix: keep literal "false" value displayed in workflow_dispatch choice dropdowns (#38080) (#38096) + * fix: parse HEAD ref (#38119) + * fix: git cmd (#38084) (#38087) + * fix(releases): generate notes for initial tag (#37697) (#37986) + * fix(actions): return 404 when job log blob is missing (#38003) (#38004) + * fix(actions): exclude `workflow_call` from workflow trigger detection (#37894) (#37899) + * fix(actions): keep action run title clickable when commit subject is a URL (#37867) (#37898) + * fix(actions): reject workflow_dispatch for workflows without that trigger (#37660) (#37895) + * fix(actions): ack re-sent `UpdateLog` finalize idempotently (#37885) (#37892) + * fix: http content file render (#37850) (#37856) + * fix(issues): clear stale ReviewTypeRequest when submitting pending review (#37809) (#37815) + * fix: Fix issue target branch selection for non-collaborators (#36916) (#38164) + +* BUILD + * fix(deps): update `@playwright/test` to 1.60.0 (#38144) + * ci: add `tools/ci-tools.ts` for the PR labeler workflow (#37831) + * fix(build): swagger css import (#37801) (#37803) + ## [1.26.2](https://github.com/go-gitea/gitea/releases/tag/1.26.2) - 2026-05-20 * SECURITY diff --git a/MAINTAINERS b/MAINTAINERS index 03ff6999f3..b1a0639e33 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -41,7 +41,6 @@ Jimmy Praet (@jpraet) Leon Hofmeister (@delvh) Wim (@42wim) Jason Song (@wolfogre) -Yarden Shoham (@yardenshoham) Yu Tian (@Zettat123) Dong Ge (@sillyguodong) Xinyi Gong (@HesterG) diff --git a/Makefile b/Makefile index 86d21dcf44..ad21a80dcf 100644 --- a/Makefile +++ b/Makefile @@ -12,13 +12,13 @@ COMMA := , XGO_VERSION := go-1.26.x AIR_PACKAGE ?= github.com/air-verse/air@v1.65.3 # renovate: datasource=go -EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.7.0 # renovate: datasource=go +EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.8.0 # 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.34.1 # renovate: datasource=go +SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.35.0 # renovate: datasource=go XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go -GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.3.0 # renovate: datasource=go +GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.5.0 # renovate: datasource=go ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d # renovate: datasource=docker @@ -127,6 +127,7 @@ BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* mo GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go SVG_DEST_DIR := public/assets/img/svg +SVG_DEST_DIRS := $(SVG_DEST_DIR) options/fileicon AIR_TMP_DIR := .air @@ -230,7 +231,9 @@ endif 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)' + @output="$$($(GO) run $(SWAGGER_PACKAGE) generate spec --enable-allof-compounding --skip-enum-desc --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)' 2>&1)" || { printf '%s\n' "$$output" >&2; exit 1; }; \ + warnings="$$(printf '%s\n' "$$output" | grep -v '^go: ')"; \ + if [ -n "$$warnings" ]; then printf '%s\n' "$$warnings" >&2; exit 1; fi .PHONY: swagger-check swagger-check: generate-swagger @@ -245,9 +248,11 @@ swagger-check: generate-swagger swagger-validate: ## check if the swagger spec is valid @# swagger "validate" requires that the "basePath" must start with a slash, but we are using Golang template "{{...}}" @$(SED_INPLACE) -E -e 's|"basePath":( *)"(.*)"|"basePath":\1"/\2"|g' './$(SWAGGER_SPEC)' # add a prefix slash to basePath - @# FIXME: there are some warnings - $(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 + @output="$$($(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)' 2>&1)"; status=$$?; \ + $(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)'; \ + printf '%s\n' "$$output" | grep -v '^go: '; \ + [ $$status -eq 0 ] || exit $$status; \ + case "$$output" in *WARNING:*) exit 1;; esac .PHONY: generate-openapi3 generate-openapi3: $(OPENAPI3_SPEC) ## generate the OpenAPI 3.0 spec from the Swagger 2.0 spec @@ -633,10 +638,10 @@ svg: node_modules ## build svg files .PHONY: svg-check svg-check: svg - @git add $(SVG_DEST_DIR) - @diff=$$(git diff --color=always --cached $(SVG_DEST_DIR)); \ + @git add $(SVG_DEST_DIRS) + @diff=$$(git diff --color=always --cached $(SVG_DEST_DIRS)); \ if [ -n "$$diff" ]; then \ - echo "Please run 'make svg' and 'git add $(SVG_DEST_DIR)' and commit the result:"; \ + echo "Please run 'make svg' and 'git add $(SVG_DEST_DIRS)' and commit the result:"; \ printf "%s" "$${diff}"; \ exit 1; \ fi diff --git a/assets/codemirror-languages.json b/assets/codemirror-languages.json index 573e7c493b..1ec42c9e05 100644 --- a/assets/codemirror-languages.json +++ b/assets/codemirror-languages.json @@ -436,6 +436,7 @@ "jsonl", "mcmeta", "sarif", + "slnlaunch", "tact", "tfstate", "topojson", @@ -691,10 +692,17 @@ "extensions": [ "ini", "cnf", + "container", "dof", "lektorproject", + "mount", + "network", "prefs", "properties", + "service", + "socket", + "target", + "timer", "url", "conf" ], diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 7b76688fe8..9e9b50c9d0 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -645,8 +645,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/v88", - "path": "github.com/google/go-github/v88/LICENSE", + "name": "github.com/google/go-github/v89", + "path": "github.com/google/go-github/v89/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" }, { @@ -699,21 +699,11 @@ "path": "github.com/gorilla/sessions/LICENSE", "licenseText": "Copyright (c) 2024 The Gorilla 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\t * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\t * 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\t * 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/hashicorp/errwrap", - "path": "github.com/hashicorp/errwrap/LICENSE", - "licenseText": "Mozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. “Contributor”\n\n means each individual or legal entity that creates, contributes to the\n creation of, or owns Covered Software.\n\n1.2. “Contributor Version”\n\n means the combination of the Contributions of others (if any) used by a\n Contributor and that particular Contributor’s Contribution.\n\n1.3. “Contribution”\n\n means Covered Software of a particular Contributor.\n\n1.4. “Covered Software”\n\n means Source Code Form to which the initial Contributor has attached the\n notice in Exhibit A, the Executable Form of such Source Code Form, and\n Modifications of such Source Code Form, in each case including portions\n thereof.\n\n1.5. “Incompatible With Secondary Licenses”\n means\n\n a. that the initial Contributor has attached the notice described in\n Exhibit B to the Covered Software; or\n\n b. that the Covered Software was made available under the terms of version\n 1.1 or earlier of the License, but not also under the terms of a\n Secondary License.\n\n1.6. “Executable Form”\n\n means any form of the work other than Source Code Form.\n\n1.7. “Larger Work”\n\n means a work that combines Covered Software with other material, in a separate\n file or files, that is not Covered Software.\n\n1.8. “License”\n\n means this document.\n\n1.9. “Licensable”\n\n means having the right to grant, to the maximum extent possible, whether at the\n time of the initial grant or subsequently, any and all of the rights conveyed by\n this License.\n\n1.10. “Modifications”\n\n means any of the following:\n\n a. any file in Source Code Form that results from an addition to, deletion\n from, or modification of the contents of Covered Software; or\n\n b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. “Patent Claims” of a Contributor\n\n means any patent claim(s), including without limitation, method, process,\n and apparatus claims, in any patent Licensable by such Contributor that\n would be infringed, but for the grant of the License, by the making,\n using, selling, offering for sale, having made, import, or transfer of\n either its Contributions or its Contributor Version.\n\n1.12. “Secondary License”\n\n means either the GNU General Public License, Version 2.0, the GNU Lesser\n General Public License, Version 2.1, the GNU Affero General Public\n License, Version 3.0, or any later versions of those licenses.\n\n1.13. “Source Code Form”\n\n means the form of the work preferred for making modifications.\n\n1.14. “You” (or “Your”)\n\n means an individual or a legal entity exercising rights under this\n License. For legal entities, “You” includes any entity that controls, is\n controlled by, or is under common control with You. For purposes of this\n definition, “control” means (a) the power, direct or indirect, to cause\n the direction or management of such entity, whether by contract or\n otherwise, or (b) ownership of more than fifty percent (50%) of the\n outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n Each Contributor hereby grants You a world-wide, royalty-free,\n non-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 as\n part of a Larger Work; and\n\n b. under Patent Claims of such Contributor to make, use, sell, offer for\n sale, have made, import, and otherwise transfer either its Contributions\n or its Contributor Version.\n\n2.2. Effective Date\n\n The licenses granted in Section 2.1 with respect to any Contribution become\n effective for each Contribution on the date the Contributor first distributes\n such Contribution.\n\n2.3. Limitations on Grant Scope\n\n The licenses granted in this Section 2 are the only rights granted under this\n License. No additional rights or licenses will be implied from the distribution\n or licensing of Covered Software under this License. Notwithstanding Section\n 2.1(b) above, no patent license is granted by a Contributor:\n\n a. for any code that a Contributor has removed from Covered Software; 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 its\n Contributions.\n\n This License does not grant any rights in the trademarks, service marks, or\n logos of any Contributor (except as may be necessary to comply with the\n notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n No Contributor makes additional grants as a result of Your choice to\n distribute the Covered Software under a subsequent version of this License\n (see Section 10.2) or under the terms of a Secondary License (if permitted\n under the terms of Section 3.3).\n\n2.5. Representation\n\n Each Contributor represents that the Contributor believes its Contributions\n are its original creation(s) or it has sufficient rights to grant the\n rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n This License is not intended to limit any rights You have under applicable\n copyright doctrines of fair use, fair dealing, or other equivalents.\n\n2.7. Conditions\n\n Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n All distribution of Covered Software in Source Code Form, including any\n Modifications that You create or to which You contribute, must be under the\n terms of this License. You must inform recipients that the Source Code Form\n of the Covered Software is governed by the terms of this License, and how\n they can obtain a copy of this License. You may not attempt to alter or\n restrict the recipients’ rights in the Source Code Form.\n\n3.2. Distribution of Executable Form\n\n If You distribute Covered Software in Executable Form then:\n\n a. such Covered Software must also be made available in Source Code Form,\n as described in Section 3.1, and You must inform recipients of the\n Executable Form how they can obtain a copy of such Source Code Form by\n reasonable means in a timely manner, at a charge no more than the cost\n of distribution to the recipient; and\n\n b. You may distribute such Executable Form under the terms of this License,\n or sublicense it under different terms, provided that the license for\n the Executable Form does not attempt to limit or alter the recipients’\n rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n You may create and distribute a Larger Work under terms of Your choice,\n provided that You also comply with the requirements of this License for the\n Covered Software. If the Larger Work is a combination of Covered Software\n with a work governed by one or more Secondary Licenses, and the Covered\n Software is not Incompatible With Secondary Licenses, this License permits\n You to additionally distribute such Covered Software under the terms of\n such Secondary License(s), so that the recipient of the Larger Work may, at\n their option, further distribute the Covered Software under the terms of\n either this License or such Secondary License(s).\n\n3.4. Notices\n\n You may not remove or alter the substance of any license notices (including\n copyright notices, patent notices, disclaimers of warranty, or limitations\n of liability) contained within the Source Code Form of the Covered\n Software, except that You may alter any license notices to the extent\n required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n You may choose to offer, and to charge a fee for, warranty, support,\n indemnity or liability obligations to one or more recipients of Covered\n Software. However, You may do so only on Your own behalf, and not on behalf\n of any Contributor. You must make it absolutely clear that any such\n warranty, support, indemnity, or liability obligation is offered by You\n alone, and You hereby agree to indemnify every Contributor for any\n liability incurred by such Contributor as a result of warranty, support,\n indemnity or liability terms You offer. You may include additional\n disclaimers of warranty and limitations of liability specific to any\n jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n If it is impossible for You to comply with any of the terms of this License\n with respect to some or all of the Covered Software due to statute, judicial\n order, or regulation then You must: (a) comply with the terms of this License\n to the maximum extent possible; and (b) describe the limitations and the code\n they affect. Such description must be placed in a text file included with all\n distributions of the Covered Software under this License. Except to the\n extent prohibited by statute or regulation, such description must be\n sufficiently detailed for a recipient of ordinary skill to be able to\n understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n fail to comply with any of its terms. However, if You become compliant,\n then the rights granted under this License from a particular Contributor\n are reinstated (a) provisionally, unless and until such Contributor\n explicitly and finally terminates Your grants, and (b) on an ongoing basis,\n if such Contributor fails to notify You of the non-compliance by some\n reasonable means prior to 60 days after You have come back into compliance.\n Moreover, Your grants from a particular Contributor are reinstated on an\n ongoing basis if such Contributor notifies You of the non-compliance by\n some reasonable means, this is the first time You have received notice of\n non-compliance with this License from such Contributor, and You become\n compliant prior to 30 days after Your receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n infringement claim (excluding declaratory judgment actions, counter-claims,\n and cross-claims) alleging that a Contributor Version directly or\n indirectly infringes any patent, then the rights granted to You by any and\n all Contributors for the Covered Software under Section 2.1 of this License\n shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n license agreements (excluding distributors and resellers) which have been\n validly granted by You or Your distributors under this License prior to\n termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n Covered Software is provided under this License on an “as is” basis, without\n warranty of any kind, either expressed, implied, or statutory, including,\n without limitation, warranties that the Covered Software is free of defects,\n merchantable, fit for a particular purpose or non-infringing. The entire\n risk as to the quality and performance of the Covered Software is with You.\n Should any Covered Software prove defective in any respect, You (not any\n Contributor) assume the cost of any necessary servicing, repair, or\n correction. This disclaimer of warranty constitutes an essential part of this\n License. No use of any Covered Software is authorized under this License\n except under this disclaimer.\n\n7. Limitation of Liability\n\n Under no circumstances and under no legal theory, whether tort (including\n negligence), contract, or otherwise, shall any Contributor, or anyone who\n distributes Covered Software as permitted above, be liable to You for any\n direct, indirect, special, incidental, or consequential damages of any\n character including, without limitation, damages for lost profits, loss of\n goodwill, work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses, even if such party shall have been\n informed of the possibility of such damages. This limitation of liability\n shall not apply to liability for death or personal injury resulting from such\n party’s negligence to the extent applicable law prohibits such limitation.\n Some jurisdictions do not allow the exclusion or limitation of incidental or\n consequential damages, so this exclusion and limitation may not apply to You.\n\n8. Litigation\n\n Any litigation relating to this License may be brought only in the courts of\n a jurisdiction where the defendant maintains its principal place of business\n and such litigation shall be governed by laws of that jurisdiction, without\n reference to its conflict-of-law provisions. Nothing in this Section shall\n prevent a party’s ability to bring cross-claims or counter-claims.\n\n9. Miscellaneous\n\n This License represents the complete agreement concerning the subject matter\n hereof. If any provision of this License is held to be unenforceable, such\n provision shall be reformed only to the extent necessary to make it\n enforceable. Any law or regulation which provides that the language of a\n contract shall be construed against the drafter shall not be used to construe\n this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n Mozilla Foundation is the license steward. Except as provided in Section\n 10.3, no one other than the license steward has the right to modify or\n publish new versions of this License. Each version will be given a\n distinguishing version number.\n\n10.2. Effect of New Versions\n\n You may distribute the Covered Software under the terms of the version of\n the License under which You originally received the Covered Software, or\n under the terms of any subsequent version published by the license\n steward.\n\n10.3. Modified Versions\n\n If you create software not governed by this License, and you want to\n create a new license for such software, you may create and use a modified\n version of this License if you rename the license and remove any\n references to the name of the license steward (except to note that such\n modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses\n If You choose to distribute Source Code Form that is Incompatible With\n Secondary Licenses under the terms of this version of the License, the\n notice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n\n This Source Code Form is subject to the\n terms of the Mozilla Public License, v.\n 2.0. If a copy of the MPL was not\n distributed with this file, You can\n obtain one at\n http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file, then\nYou may include the notice in a location (such as a LICENSE file in a relevant\ndirectory) where a recipient would be likely to look for such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - “Incompatible With Secondary Licenses” Notice\n\n This Source Code Form is “Incompatible\n With Secondary Licenses”, as defined by\n the Mozilla Public License, v. 2.0.\n\n" - }, { "name": "github.com/hashicorp/go-cleanhttp", "path": "github.com/hashicorp/go-cleanhttp/LICENSE", "licenseText": "Mozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. \"Contributor\"\n\n means each individual or legal entity that creates, contributes to the\n creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n\n means the combination of the Contributions of others (if any) used by a\n Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n\n means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n\n means Source Code Form to which the initial Contributor has attached the\n notice in Exhibit A, the Executable Form of such Source Code Form, and\n Modifications of such Source Code Form, in each case including portions\n thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n means\n\n a. that the initial Contributor has attached the notice described in\n 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 terms of\n a Secondary License.\n\n1.6. \"Executable Form\"\n\n means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n\n means a work that combines Covered Software with other material, in a\n separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n\n means this document.\n\n1.9. \"Licensable\"\n\n means having the right to grant, to the maximum extent possible, whether\n at the time of the initial grant or subsequently, any and all of the\n rights conveyed by this License.\n\n1.10. \"Modifications\"\n\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 Software; or\n\n b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. \"Patent Claims\" of a Contributor\n\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 License,\n by the making, using, selling, offering for sale, having made, import,\n or transfer of either its Contributions or its Contributor Version.\n\n1.12. \"Secondary License\"\n\n means either the GNU General Public License, Version 2.0, the GNU Lesser\n General Public License, Version 2.1, the GNU Affero General Public\n License, Version 3.0, or any later versions of those licenses.\n\n1.13. \"Source Code Form\"\n\n means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n\n means an individual or a legal entity exercising rights under this\n License. For legal entities, \"You\" includes any entity that controls, is\n controlled by, or is under common control with You. For purposes of this\n definition, \"control\" means (a) the power, direct or indirect, to cause\n the direction or management of such entity, whether by contract or\n otherwise, or (b) ownership of more than fifty percent (50%) of the\n outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n Each Contributor hereby grants You a world-wide, royalty-free,\n non-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 for\n sale, have made, import, and otherwise transfer either its\n Contributions or its Contributor Version.\n\n2.2. Effective Date\n\n The licenses granted in Section 2.1 with respect to any Contribution\n become effective for each Contribution on the date the Contributor first\n distributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\n The licenses granted in this Section 2 are the only rights granted under\n this License. No additional rights or licenses will be implied from the\n distribution or licensing of Covered Software under this License.\n Notwithstanding Section 2.1(b) above, no patent license is granted by a\n Contributor:\n\n a. for any code that a Contributor has removed from Covered Software; 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\n This License does not grant any rights in the trademarks, service marks,\n or logos of any Contributor (except as may be necessary to comply with\n the notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n No Contributor makes additional grants as a result of Your choice to\n distribute the Covered Software under a subsequent version of this\n License (see Section 10.2) or under the terms of a Secondary License (if\n permitted under the terms of Section 3.3).\n\n2.5. Representation\n\n Each Contributor represents that the Contributor believes its\n Contributions are its original creation(s) or it has sufficient rights to\n grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n This License is not intended to limit any rights You have under\n applicable copyright doctrines of fair use, fair dealing, or other\n equivalents.\n\n2.7. Conditions\n\n Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n All distribution of Covered Software in Source Code Form, including any\n Modifications that You create or to which You contribute, must be under\n the terms of this License. You must inform recipients that the Source\n Code Form of the Covered Software is governed by the terms of this\n License, and how they can obtain a copy of this License. You may not\n attempt to alter or restrict the recipients' rights in the Source Code\n Form.\n\n3.2. Distribution of Executable Form\n\n If You distribute Covered Software in Executable Form then:\n\n a. such Covered Software must also be made available in Source Code Form,\n as described in Section 3.1, and You must inform recipients of the\n Executable Form how they can obtain a copy of such Source Code Form by\n reasonable means in a timely manner, at a charge no more than the cost\n 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 the\n recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n You may create and distribute a Larger Work under terms of Your choice,\n provided that You also comply with the requirements of this License for\n the Covered Software. If the Larger Work is a combination of Covered\n Software with a work governed by one or more Secondary Licenses, and the\n Covered Software is not Incompatible With Secondary Licenses, this\n License permits You to additionally distribute such Covered Software\n under the terms of such Secondary License(s), so that the recipient of\n the Larger Work may, at their option, further distribute the Covered\n Software under the terms of either this License or such Secondary\n License(s).\n\n3.4. Notices\n\n You may not remove or alter the substance of any license notices\n (including copyright notices, patent notices, disclaimers of warranty, or\n limitations of liability) contained within the Source Code Form of the\n Covered Software, except that You may alter any license notices to the\n extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n You may choose to offer, and to charge a fee for, warranty, support,\n indemnity or liability obligations to one or more recipients of Covered\n Software. However, You may do so only on Your own behalf, and not on\n behalf of any Contributor. You must make it absolutely clear that any\n such warranty, support, indemnity, or liability obligation is offered by\n You alone, and You hereby agree to indemnify every Contributor for any\n liability incurred by such Contributor as a result of warranty, support,\n indemnity or liability terms You offer. You may include additional\n disclaimers of warranty and limitations of liability specific to any\n jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n If it is impossible for You to comply with any of the terms of this License\n with respect to some or all of the Covered Software due to statute,\n judicial order, or regulation then You must: (a) comply with the terms of\n this License to the maximum extent possible; and (b) describe the\n limitations and the code they affect. Such description must be placed in a\n text file included with all distributions of the Covered Software under\n this License. Except to the extent prohibited by statute or regulation,\n such description must be sufficiently detailed for a recipient of ordinary\n skill to be able to understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n fail to comply with any of its terms. However, if You become compliant,\n then the rights granted under this License from a particular Contributor\n are reinstated (a) provisionally, unless and until such Contributor\n explicitly and finally terminates Your grants, and (b) on an ongoing\n basis, if such Contributor fails to notify You of the non-compliance by\n some reasonable means prior to 60 days after You have come back into\n compliance. Moreover, Your grants from a particular Contributor are\n reinstated on an ongoing basis if such Contributor notifies You of the\n non-compliance by some reasonable means, this is the first time You have\n received notice of non-compliance with this License from such\n Contributor, and You become compliant prior to 30 days after Your receipt\n of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n infringement claim (excluding declaratory judgment actions,\n counter-claims, and cross-claims) alleging that a Contributor Version\n directly or indirectly infringes any patent, then the rights granted to\n You by any and all Contributors for the Covered Software under Section\n 2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n license agreements (excluding distributors and resellers) which have been\n validly granted by You or Your distributors under this License prior to\n termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n Covered Software is provided under this License on an \"as is\" basis,\n without warranty of any kind, either expressed, implied, or statutory,\n including, without limitation, warranties that the Covered Software is free\n of defects, merchantable, fit for a particular purpose or non-infringing.\n The entire risk as to the quality and performance of the Covered Software\n is with You. Should any Covered Software prove defective in any respect,\n You (not any Contributor) assume the cost of any necessary servicing,\n repair, or correction. This disclaimer of warranty constitutes an essential\n part of this License. No use of any Covered Software is authorized under\n this License except under this disclaimer.\n\n7. Limitation of Liability\n\n Under no circumstances and under no legal theory, whether tort (including\n negligence), contract, or otherwise, shall any Contributor, or anyone who\n distributes Covered Software as permitted above, be liable to You for any\n direct, indirect, special, incidental, or consequential damages of any\n character including, without limitation, damages for lost profits, loss of\n goodwill, work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses, even if such party shall have been\n informed of the possibility of such damages. This limitation of liability\n shall not apply to liability for death or personal injury resulting from\n such party's negligence to the extent applicable law prohibits such\n limitation. Some jurisdictions do not allow the exclusion or limitation of\n incidental or consequential damages, so this exclusion and limitation may\n not apply to You.\n\n8. Litigation\n\n Any litigation relating to this License may be brought only in the courts\n of a jurisdiction where the defendant maintains its principal place of\n business and such litigation shall be governed by laws of that\n jurisdiction, without reference to its conflict-of-law provisions. Nothing\n in this Section shall prevent a party's ability to bring cross-claims or\n counter-claims.\n\n9. Miscellaneous\n\n This License represents the complete agreement concerning the subject\n matter hereof. If any provision of this License is held to be\n unenforceable, such provision shall be reformed only to the extent\n necessary to make it enforceable. Any law or regulation which provides that\n the language of a contract shall be construed against the drafter shall not\n be used to construe this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n Mozilla Foundation is the license steward. Except as provided in Section\n 10.3, no one other than the license steward has the right to modify or\n publish new versions of this License. Each version will be given a\n distinguishing version number.\n\n10.2. Effect of New Versions\n\n You may distribute the Covered Software under the terms of the version\n of the License under which You originally received the Covered Software,\n or under the terms of any subsequent version published by the license\n steward.\n\n10.3. Modified Versions\n\n If you create software not governed by this License, and you want to\n create a new license for such software, you may create and use a\n modified version of this License if you rename the license and remove\n any references to the name of the license steward (except to note that\n such modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\n Licenses If You choose to distribute Source Code Form that is\n Incompatible With Secondary Licenses under the terms of this version of\n the License, the notice described in Exhibit B of this License must be\n attached.\n\nExhibit A - Source Code Form License Notice\n\n This Source Code Form is subject to the\n terms of the Mozilla Public License, v.\n 2.0. If a copy of the MPL was not\n distributed with this file, You can\n obtain one at\n http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file,\nthen You may include the notice in a location (such as a LICENSE file in a\nrelevant directory) where a recipient would be likely to look for such a\nnotice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n\n This Source Code Form is \"Incompatible\n With Secondary Licenses\", as defined by\n the Mozilla Public License, v. 2.0.\n\n" }, - { - "name": "github.com/hashicorp/go-multierror", - "path": "github.com/hashicorp/go-multierror/LICENSE", - "licenseText": "Mozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. “Contributor”\n\n means each individual or legal entity that creates, contributes to the\n creation of, or owns Covered Software.\n\n1.2. “Contributor Version”\n\n means the combination of the Contributions of others (if any) used by a\n Contributor and that particular Contributor’s Contribution.\n\n1.3. “Contribution”\n\n means Covered Software of a particular Contributor.\n\n1.4. “Covered Software”\n\n means Source Code Form to which the initial Contributor has attached the\n notice in Exhibit A, the Executable Form of such Source Code Form, and\n Modifications of such Source Code Form, in each case including portions\n thereof.\n\n1.5. “Incompatible With Secondary Licenses”\n means\n\n a. that the initial Contributor has attached the notice described in\n Exhibit B to the Covered Software; or\n\n b. that the Covered Software was made available under the terms of version\n 1.1 or earlier of the License, but not also under the terms of a\n Secondary License.\n\n1.6. “Executable Form”\n\n means any form of the work other than Source Code Form.\n\n1.7. “Larger Work”\n\n means a work that combines Covered Software with other material, in a separate\n file or files, that is not Covered Software.\n\n1.8. “License”\n\n means this document.\n\n1.9. “Licensable”\n\n means having the right to grant, to the maximum extent possible, whether at the\n time of the initial grant or subsequently, any and all of the rights conveyed by\n this License.\n\n1.10. “Modifications”\n\n means any of the following:\n\n a. any file in Source Code Form that results from an addition to, deletion\n from, or modification of the contents of Covered Software; or\n\n b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. “Patent Claims” of a Contributor\n\n means any patent claim(s), including without limitation, method, process,\n and apparatus claims, in any patent Licensable by such Contributor that\n would be infringed, but for the grant of the License, by the making,\n using, selling, offering for sale, having made, import, or transfer of\n either its Contributions or its Contributor Version.\n\n1.12. “Secondary License”\n\n means either the GNU General Public License, Version 2.0, the GNU Lesser\n General Public License, Version 2.1, the GNU Affero General Public\n License, Version 3.0, or any later versions of those licenses.\n\n1.13. “Source Code Form”\n\n means the form of the work preferred for making modifications.\n\n1.14. “You” (or “Your”)\n\n means an individual or a legal entity exercising rights under this\n License. For legal entities, “You” includes any entity that controls, is\n controlled by, or is under common control with You. For purposes of this\n definition, “control” means (a) the power, direct or indirect, to cause\n the direction or management of such entity, whether by contract or\n otherwise, or (b) ownership of more than fifty percent (50%) of the\n outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n Each Contributor hereby grants You a world-wide, royalty-free,\n non-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 as\n part of a Larger Work; and\n\n b. under Patent Claims of such Contributor to make, use, sell, offer for\n sale, have made, import, and otherwise transfer either its Contributions\n or its Contributor Version.\n\n2.2. Effective Date\n\n The licenses granted in Section 2.1 with respect to any Contribution become\n effective for each Contribution on the date the Contributor first distributes\n such Contribution.\n\n2.3. Limitations on Grant Scope\n\n The licenses granted in this Section 2 are the only rights granted under this\n License. No additional rights or licenses will be implied from the distribution\n or licensing of Covered Software under this License. Notwithstanding Section\n 2.1(b) above, no patent license is granted by a Contributor:\n\n a. for any code that a Contributor has removed from Covered Software; 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 its\n Contributions.\n\n This License does not grant any rights in the trademarks, service marks, or\n logos of any Contributor (except as may be necessary to comply with the\n notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n No Contributor makes additional grants as a result of Your choice to\n distribute the Covered Software under a subsequent version of this License\n (see Section 10.2) or under the terms of a Secondary License (if permitted\n under the terms of Section 3.3).\n\n2.5. Representation\n\n Each Contributor represents that the Contributor believes its Contributions\n are its original creation(s) or it has sufficient rights to grant the\n rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n This License is not intended to limit any rights You have under applicable\n copyright doctrines of fair use, fair dealing, or other equivalents.\n\n2.7. Conditions\n\n Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n All distribution of Covered Software in Source Code Form, including any\n Modifications that You create or to which You contribute, must be under the\n terms of this License. You must inform recipients that the Source Code Form\n of the Covered Software is governed by the terms of this License, and how\n they can obtain a copy of this License. You may not attempt to alter or\n restrict the recipients’ rights in the Source Code Form.\n\n3.2. Distribution of Executable Form\n\n If You distribute Covered Software in Executable Form then:\n\n a. such Covered Software must also be made available in Source Code Form,\n as described in Section 3.1, and You must inform recipients of the\n Executable Form how they can obtain a copy of such Source Code Form by\n reasonable means in a timely manner, at a charge no more than the cost\n of distribution to the recipient; and\n\n b. You may distribute such Executable Form under the terms of this License,\n or sublicense it under different terms, provided that the license for\n the Executable Form does not attempt to limit or alter the recipients’\n rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n You may create and distribute a Larger Work under terms of Your choice,\n provided that You also comply with the requirements of this License for the\n Covered Software. If the Larger Work is a combination of Covered Software\n with a work governed by one or more Secondary Licenses, and the Covered\n Software is not Incompatible With Secondary Licenses, this License permits\n You to additionally distribute such Covered Software under the terms of\n such Secondary License(s), so that the recipient of the Larger Work may, at\n their option, further distribute the Covered Software under the terms of\n either this License or such Secondary License(s).\n\n3.4. Notices\n\n You may not remove or alter the substance of any license notices (including\n copyright notices, patent notices, disclaimers of warranty, or limitations\n of liability) contained within the Source Code Form of the Covered\n Software, except that You may alter any license notices to the extent\n required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n You may choose to offer, and to charge a fee for, warranty, support,\n indemnity or liability obligations to one or more recipients of Covered\n Software. However, You may do so only on Your own behalf, and not on behalf\n of any Contributor. You must make it absolutely clear that any such\n warranty, support, indemnity, or liability obligation is offered by You\n alone, and You hereby agree to indemnify every Contributor for any\n liability incurred by such Contributor as a result of warranty, support,\n indemnity or liability terms You offer. You may include additional\n disclaimers of warranty and limitations of liability specific to any\n jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n If it is impossible for You to comply with any of the terms of this License\n with respect to some or all of the Covered Software due to statute, judicial\n order, or regulation then You must: (a) comply with the terms of this License\n to the maximum extent possible; and (b) describe the limitations and the code\n they affect. Such description must be placed in a text file included with all\n distributions of the Covered Software under this License. Except to the\n extent prohibited by statute or regulation, such description must be\n sufficiently detailed for a recipient of ordinary skill to be able to\n understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n fail to comply with any of its terms. However, if You become compliant,\n then the rights granted under this License from a particular Contributor\n are reinstated (a) provisionally, unless and until such Contributor\n explicitly and finally terminates Your grants, and (b) on an ongoing basis,\n if such Contributor fails to notify You of the non-compliance by some\n reasonable means prior to 60 days after You have come back into compliance.\n Moreover, Your grants from a particular Contributor are reinstated on an\n ongoing basis if such Contributor notifies You of the non-compliance by\n some reasonable means, this is the first time You have received notice of\n non-compliance with this License from such Contributor, and You become\n compliant prior to 30 days after Your receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n infringement claim (excluding declaratory judgment actions, counter-claims,\n and cross-claims) alleging that a Contributor Version directly or\n indirectly infringes any patent, then the rights granted to You by any and\n all Contributors for the Covered Software under Section 2.1 of this License\n shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n license agreements (excluding distributors and resellers) which have been\n validly granted by You or Your distributors under this License prior to\n termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n Covered Software is provided under this License on an “as is” basis, without\n warranty of any kind, either expressed, implied, or statutory, including,\n without limitation, warranties that the Covered Software is free of defects,\n merchantable, fit for a particular purpose or non-infringing. The entire\n risk as to the quality and performance of the Covered Software is with You.\n Should any Covered Software prove defective in any respect, You (not any\n Contributor) assume the cost of any necessary servicing, repair, or\n correction. This disclaimer of warranty constitutes an essential part of this\n License. No use of any Covered Software is authorized under this License\n except under this disclaimer.\n\n7. Limitation of Liability\n\n Under no circumstances and under no legal theory, whether tort (including\n negligence), contract, or otherwise, shall any Contributor, or anyone who\n distributes Covered Software as permitted above, be liable to You for any\n direct, indirect, special, incidental, or consequential damages of any\n character including, without limitation, damages for lost profits, loss of\n goodwill, work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses, even if such party shall have been\n informed of the possibility of such damages. This limitation of liability\n shall not apply to liability for death or personal injury resulting from such\n party’s negligence to the extent applicable law prohibits such limitation.\n Some jurisdictions do not allow the exclusion or limitation of incidental or\n consequential damages, so this exclusion and limitation may not apply to You.\n\n8. Litigation\n\n Any litigation relating to this License may be brought only in the courts of\n a jurisdiction where the defendant maintains its principal place of business\n and such litigation shall be governed by laws of that jurisdiction, without\n reference to its conflict-of-law provisions. Nothing in this Section shall\n prevent a party’s ability to bring cross-claims or counter-claims.\n\n9. Miscellaneous\n\n This License represents the complete agreement concerning the subject matter\n hereof. If any provision of this License is held to be unenforceable, such\n provision shall be reformed only to the extent necessary to make it\n enforceable. Any law or regulation which provides that the language of a\n contract shall be construed against the drafter shall not be used to construe\n this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n Mozilla Foundation is the license steward. Except as provided in Section\n 10.3, no one other than the license steward has the right to modify or\n publish new versions of this License. Each version will be given a\n distinguishing version number.\n\n10.2. Effect of New Versions\n\n You may distribute the Covered Software under the terms of the version of\n the License under which You originally received the Covered Software, or\n under the terms of any subsequent version published by the license\n steward.\n\n10.3. Modified Versions\n\n If you create software not governed by this License, and you want to\n create a new license for such software, you may create and use a modified\n version of this License if you rename the license and remove any\n references to the name of the license steward (except to note that such\n modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses\n If You choose to distribute Source Code Form that is Incompatible With\n Secondary Licenses under the terms of this version of the License, the\n notice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n\n This Source Code Form is subject to the\n terms of the Mozilla Public License, v.\n 2.0. If a copy of the MPL was not\n distributed with this file, You can\n obtain one at\n http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file, then\nYou may include the notice in a location (such as a LICENSE file in a relevant\ndirectory) where a recipient would be likely to look for such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - “Incompatible With Secondary Licenses” Notice\n\n This Source Code Form is “Incompatible\n With Secondary Licenses”, as defined by\n the Mozilla Public License, v. 2.0.\n" - }, { "name": "github.com/hashicorp/go-retryablehttp", "path": "github.com/hashicorp/go-retryablehttp/LICENSE", diff --git a/cmd/admin.go b/cmd/admin.go index 2396bbb47c..14cdc81e37 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -128,8 +128,8 @@ func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error { } log.Trace("Processing next %d repos of %d", len(repos), count) for _, repo := range repos { - log.Trace("Synchronizing repo %s with path %s", repo.FullName(), repo.RelativePath()) - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + log.Trace("Synchronizing repo %s", repo.FullName()) + gitRepo, err := gitrepo.OpenRepository(repo) if err != nil { log.Warn("OpenRepository: %v", err) continue diff --git a/cmd/admin_user.go b/cmd/admin_user.go index 8dd8bb4eca..b45aee1895 100644 --- a/cmd/admin_user.go +++ b/cmd/admin_user.go @@ -18,6 +18,7 @@ func newUserCommand() *cli.Command { microcmdUserDelete(), newUserGenerateAccessTokenCommand(), microcmdUserMustChangePassword(), + microcmdUserDisableTwoFactor(), }, } } diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index 340ea072d5..81b6f87a31 100644 --- a/cmd/admin_user_create.go +++ b/cmd/admin_user_create.go @@ -158,7 +158,8 @@ func runCreateUser(ctx context.Context, c *cli.Command) error { } isAdmin := c.Bool("admin") - mustChangePassword := true // always default to true + // Only local, existing, regular users should be forced to update their password. Bot users for example are non-interactive + mustChangePassword := userType == user_model.UserTypeIndividual if c.IsSet("must-change-password") { if userType != user_model.UserTypeIndividual { return errors.New("must-change-password flag can only be set for individual users") diff --git a/cmd/admin_user_create_test.go b/cmd/admin_user_create_test.go index f3794c5dff..ece2e8869d 100644 --- a/cmd/admin_user_create_test.go +++ b/cmd/admin_user_create_test.go @@ -63,6 +63,7 @@ func TestAdminUserCreate(t *testing.T) { u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"}) assert.Equal(t, user_model.UserTypeBot, u.Type) assert.Empty(t, u.Passwd) + assert.False(t, u.MustChangePassword, "bot users should not be forced to change password") }) t.Run("AccessToken", func(t *testing.T) { diff --git a/cmd/admin_user_disable_2fa.go b/cmd/admin_user_disable_2fa.go new file mode 100644 index 0000000000..1e8502cf04 --- /dev/null +++ b/cmd/admin_user_disable_2fa.go @@ -0,0 +1,72 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "context" + "errors" + "fmt" + "strings" + + auth_model "gitea.dev/models/auth" + user_model "gitea.dev/models/user" + "gitea.dev/modules/setting" + + "github.com/urfave/cli/v3" +) + +func microcmdUserDisableTwoFactor() *cli.Command { + return &cli.Command{ + Name: "disable-2fa", + Usage: "Disable two-factor authentication for a user", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Usage: "Username of the user to disable 2FA for", + }, + &cli.Int64Flag{ + Name: "id", + Usage: "ID of the user to disable 2FA for", + }, + }, + Action: runDisableTwoFactor, + } +} + +func runDisableTwoFactor(ctx context.Context, c *cli.Command) error { + if !c.IsSet("id") && !c.IsSet("username") { + return errors.New("either --id or --username must be provided") + } + + if !setting.IsInTesting { + if err := initDB(ctx); err != nil { + return err + } + } + + var user *user_model.User + var err error + if c.IsSet("id") { + user, err = user_model.GetUserByID(ctx, c.Int64("id")) + } else { + user, err = user_model.GetUserByName(ctx, c.String("username")) + } + if err != nil { + return err + } + + // When both selectors are given, make sure they refer to the same user. + if c.IsSet("id") && c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) { + return fmt.Errorf("the user with id %d is %q, which does not match the provided username %q", user.ID, user.Name, c.String("username")) + } + + totp, webAuthn, err := auth_model.DisableTwoFactor(ctx, user.ID) + if err != nil { + return err + } + + fmt.Printf("Disabled 2FA for user %q (removed %d TOTP and %d WebAuthn credential(s))\n", user.Name, totp, webAuthn) + return nil +} diff --git a/cmd/admin_user_disable_2fa_test.go b/cmd/admin_user_disable_2fa_test.go new file mode 100644 index 0000000000..4fe94974bb --- /dev/null +++ b/cmd/admin_user_disable_2fa_test.go @@ -0,0 +1,119 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "io" + "strconv" + "testing" + + auth_model "gitea.dev/models/auth" + "gitea.dev/models/db" + "gitea.dev/models/unittest" + user_model "gitea.dev/models/user" + + "github.com/go-webauthn/webauthn/webauthn" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDisableTwoFactorCommand(t *testing.T) { + ctx := t.Context() + + defer func() { + require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}, &auth_model.TwoFactor{}, &auth_model.WebAuthnCredential{})) + }() + + t.Run("disable TOTP and WebAuthn", func(t *testing.T) { + require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "tfuser", "--email", "tfuser@gitea.local", "--random-password"})) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "tfuser"}) + + // Enroll TOTP. + tf := &auth_model.TwoFactor{UID: user.ID} + require.NoError(t, tf.SetSecret("test-secret")) + _, err := tf.GenerateScratchToken() + require.NoError(t, err) + require.NoError(t, auth_model.NewTwoFactor(ctx, tf)) + + // Register a WebAuthn credential. + _, err = auth_model.CreateCredential(ctx, user.ID, "test-key", &webauthn.Credential{ID: []byte("test-cred-id")}) + require.NoError(t, err) + + has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, user.ID) + require.NoError(t, err) + require.True(t, has) + + require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--username", "tfuser"})) + + // Both factors must be gone afterwards. + has, err = auth_model.HasTwoFactorOrWebAuthn(ctx, user.ID) + require.NoError(t, err) + assert.False(t, has) + }) + + t.Run("disable by id", func(t *testing.T) { + require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "iduser", "--email", "iduser@gitea.local", "--random-password"})) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "iduser"}) + + tf := &auth_model.TwoFactor{UID: user.ID} + require.NoError(t, tf.SetSecret("test-secret")) + require.NoError(t, auth_model.NewTwoFactor(ctx, tf)) + + require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--id", strconv.FormatInt(user.ID, 10)})) + + has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, user.ID) + require.NoError(t, err) + assert.False(t, has) + }) + + t.Run("no enrollment is a no-op", func(t *testing.T) { + require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "plainuser", "--email", "plainuser@gitea.local", "--random-password"})) + require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--username", "plainuser"})) + }) + + t.Run("id and username must match when both given", func(t *testing.T) { + require.NoError(t, microcmdUserCreate().Run(ctx, []string{"create", "--username", "matchuser", "--email", "matchuser@gitea.local", "--random-password"})) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "matchuser"}) + id := strconv.FormatInt(user.ID, 10) + + // Matching id + username is accepted. + require.NoError(t, microcmdUserDisableTwoFactor().Run(ctx, []string{"disable-2fa", "--id", id, "--username", "matchuser"})) + + // Mismatched id + username is rejected. + cmd := microcmdUserDisableTwoFactor() + cmd.Writer, cmd.ErrWriter = io.Discard, io.Discard + err := cmd.Run(ctx, []string{"disable-2fa", "--id", id, "--username", "someotheruser"}) + require.Error(t, err) + require.Contains(t, err.Error(), "does not match the provided username") + }) + + t.Run("failure cases", func(t *testing.T) { + testCases := []struct { + name string + args []string + expectedErr string + }{ + { + name: "user does not exist", + args: []string{"disable-2fa", "--username", "nonexistentuser"}, + expectedErr: "user does not exist", + }, + { + name: "neither id nor username", + args: []string{"disable-2fa"}, + expectedErr: "either --id or --username must be provided", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cmd := microcmdUserDisableTwoFactor() + 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/cmdtest/cmd_test.go b/cmd/cmdtest/cmd_test.go index 6d4937fa11..f41793c8e4 100644 --- a/cmd/cmdtest/cmd_test.go +++ b/cmd/cmdtest/cmd_test.go @@ -203,7 +203,7 @@ func TestCliCmdError(t *testing.T) { assert.Error(t, err) assert.Equal(t, 1, r.ExitCode) assert.Empty(t, r.Stdout) - assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr) + assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n", r.Stderr) app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }}) r, err = runTestApp(app, "./gitea", "test-cmd") @@ -235,3 +235,14 @@ func TestCliCmdBefore(t *testing.T) { assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config") assert.Equal(t, "/dev/null", configValues["action"]) } + +func TestCliCmdCompletion(t *testing.T) { + app := newTestApp(cli.Command{ + Action: func(ctx context.Context, cmd *cli.Command) error { return nil }, + }) + res, err := runTestApp(app, "./gitea", "completion", "bash", "--nonexist") + assert.Error(t, err) + assert.Equal(t, 1, res.ExitCode) + assert.Equal(t, "", res.Stdout) + assert.Equal(t, "Incorrect Usage: flag provided but not defined: -nonexist\n", res.Stderr) +} diff --git a/cmd/main.go b/cmd/main.go index 243995d634..66b2fba461 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,10 +5,10 @@ package cmd import ( "context" + "errors" "fmt" "io" "os" - "strings" "gitea.dev/modules/log" "gitea.dev/modules/setting" @@ -154,16 +154,37 @@ func NewMainApp(appVer AppVersion) *cli.Command { return app } +// usageErr marks a usage error already reported by cliOnUsageError, so RunMainApp does not print it again. +type usageErr struct{ err error } + +func (e usageErr) Error() string { return e.err.Error() } +func (e usageErr) Unwrap() error { return e.err } + +// cliOnUsageError reports usage errors itself instead of letting urfave/cli dump the full help to stdout (since urfave/cli v3.10). +func cliOnUsageError(_ context.Context, cmd *cli.Command, err error, _ bool) error { + _, _ = fmt.Fprintf(cmd.Root().ErrWriter, "Incorrect Usage: %s\n", err.Error()) + return usageErr{err} +} + +func setCLIOnUsageError(cmd *cli.Command) { + _ = cmd.Walk(func(c *cli.Command) error { + c.OnUsageError = cliOnUsageError + return nil + }) +} + func RunMainApp(app *cli.Command, args ...string) error { ctx, cancel := installSignals() defer cancel() + setCLIOnUsageError(app) + // the completion subcommands are built during app.Run, after the Walk above, so cover them via this hook + app.ConfigureShellCompletionCommand = setCLIOnUsageError err := app.Run(ctx, args) if err == nil { return nil } - if strings.HasPrefix(err.Error(), "flag provided but not defined:") { - // the cli package should already have output the error message, so just exit - cli.OsExiter(1) + if _, ok := errors.AsType[usageErr](err); ok { + cli.OsExiter(1) // cliOnUsageError already reported it return err } _, _ = fmt.Fprintf(app.ErrWriter, "Command error: %v\n", err) diff --git a/cmd/serv.go b/cmd/serv.go index b39076f6a7..eddbbcb3ab 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -201,35 +201,27 @@ func runServ(ctx context.Context, c *cli.Command) error { return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd) } - repoPath := strings.TrimPrefix(sshCmdArgs[1], "/") - repoPathFields := strings.SplitN(repoPath, "/", 2) - if len(repoPathFields) != 2 { - return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath) + var reqOwnerName, reqRepoName string + { + var ok bool + reqRepoPath := strings.TrimPrefix(sshCmdArgs[1], "/") + reqOwnerName, reqRepoName, ok = strings.Cut(reqRepoPath, "/") + if !ok { + return fail(ctx, "Invalid repository path", "Invalid repository path: %v", reqRepoPath) + } + reqRepoName = strings.TrimSuffix(reqRepoName, ".git") // "the-repo-name" or "the-repo-name.wiki" } - username := repoPathFields[0] - reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki" - - if !repo_model.IsValidSSHAccessRepoName(reponame) { - return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame) + if !repo_model.IsValidSSHAccessRepoName(reqRepoName) { + return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reqRepoName) } if c.Bool("enable-pprof") { - if err := os.MkdirAll(setting.PprofDataPath, os.ModePerm); err != nil { - return fail(ctx, "Error while trying to create PPROF_DATA_PATH", "Error while trying to create PPROF_DATA_PATH: %v", err) - } - - stopCPUProfiler, err := pprof.DumpCPUProfileForUsername(setting.PprofDataPath, username) + stopProfiler, err := pprof.DumpPprofForUsername(setting.PprofDataPath, reqOwnerName) if err != nil { - return fail(ctx, "Unable to start CPU profiler", "Unable to start CPU profile: %v", err) + return fail(ctx, "Unable to start pprof profiler", "Unable to start pprof profile: %v", err) } - defer func() { - stopCPUProfiler() - err := pprof.DumpMemProfileForUsername(setting.PprofDataPath, username) - if err != nil { - _ = fail(ctx, "Unable to dump Mem profile", "Unable to dump Mem Profile: %v", err) - } - }() + defer stopProfiler() } verb, lfsVerb := sshCmdArgs[0], "" @@ -254,30 +246,29 @@ func runServ(ctx context.Context, c *cli.Command) error { return fail(ctx, "Unknown git command", "Unknown git command %s %s", verb, lfsVerb) } - results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb) + results, extra := private.ServCommand(ctx, keyID, reqOwnerName, reqRepoName, requestedMode, verb, lfsVerb) if extra.HasError() { return fail(ctx, extra.UserMsg, "ServCommand failed: %s", extra.Error) } - // because the original repoPath maybe redirected, we need to use the returned actual repository information - if results.IsWiki { - repoPath = repo_model.RelativeWikiPath(results.OwnerName, results.RepoName) - } else { - repoPath = repo_model.RelativePath(results.OwnerName, results.RepoName) - } - // LFS SSH protocol if verb == git.CmdVerbLfsTransfer { + if results.IsWiki { + return fail(ctx, "LFS Transfer is not supported for wikis", "") + } token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID}) if err != nil { return err } - return lfstransfer.Main(ctx, repoPath, lfsVerb, token) + return lfstransfer.Main(ctx, results.OwnerName, results.RepoName, lfsVerb, token) } // LFS token authentication if verb == git.CmdVerbLfsAuthenticate { - url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName)) + if results.IsWiki { + return fail(ctx, "LFS Authenticate is not supported for wikis", "") + } + lfsTokenHref := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName)) token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID}) if err != nil { @@ -286,7 +277,7 @@ func runServ(ctx context.Context, c *cli.Command) error { tokenAuthentication := &git_model.LFSTokenResponse{ Header: make(map[string]string), - Href: url, + Href: lfsTokenHref, } tokenAuthentication.Header["Authorization"] = token @@ -307,12 +298,12 @@ func runServ(ctx context.Context, c *cli.Command) error { verbFields := strings.SplitN(verb, "-", 2) if len(verbFields) == 2 { // use git binary with the sub-command part: "C:\...\bin\git.exe", "upload-pack", ... - command = exec.CommandContext(ctx, gitcmd.GitExecutable, verbFields[1], repoPath) + command = exec.CommandContext(ctx, gitcmd.GitExecutable, verbFields[1], results.RepoStoragePath) } } if command == nil { // by default, use the verb (it has been checked above by allowedCommands) - command = exec.CommandContext(ctx, gitBinVerb, repoPath) + command = exec.CommandContext(ctx, gitBinVerb, results.RepoStoragePath) } process.SetSysProcAttribute(command) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index fffd6eeb72..c01ea5af74 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -536,6 +536,13 @@ INTERNAL_TOKEN = ;; Leave it empty to apply the default policy, or set it to "unset" to disable Content-Security-Policy. ;CONTENT_SECURITY_POLICY_GENERAL = +;; Webhook and oauth2 clients can only call allowed hosts for security reasons. Comma separated list, eg: external, 192.168.1.0/24, *.mydomain.com +;; Built-in: loopback (for localhost), private (for LAN/intranet), external (for public hosts on internet), * (for all hosts) +;; CIDR list: 1.2.3.0/8, 2001:db8::/32 +;; Wildcard hosts: *.mydomain.com, 192.168.100.* +;; This list is enforced on direct connections only. When an HTTP proxy is configured, restricting the proxied target is the proxy server's responsibility. +;ALLOWED_HOST_LIST = external + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; [camo] @@ -762,7 +769,12 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Git config options -;; This section only does "set" config, a removed config key from this section won't be removed from git config automatically. The format is `some.configKey = value`. +;; The format is `some.configKey = value`. +;; These options will be written into the gitconfig file under "[git] HOME_PATH" when Gitea web starts. +;; ATTENTION: +;; * It only does "set" config, a removed config key from this section won't be removed from git config automatically. +;; * Some config options might affect the behavior of git and fail Gitea's git operation, +;; make sure you know what you are doing before making changes. ;[git.config] ;diff.algorithm = histogram ;core.logAllRefUpdates = true @@ -1186,6 +1198,7 @@ LEVEL = Info ;; 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. +;; "branch-name" always uses the PR's branch name. ;DEFAULT_TITLE_SOURCE = auto ;; ;; 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. @@ -1749,13 +1762,6 @@ LEVEL = Info ;; Deliver timeout in seconds ;DELIVER_TIMEOUT = 5 ;; -;; Webhook can only call allowed hosts for security reasons. Comma separated list, eg: external, 192.168.1.0/24, *.mydomain.com -;; Built-in: loopback (for localhost), private (for LAN/intranet), external (for public hosts on internet), * (for all hosts) -;; CIDR list: 1.2.3.0/8, 2001:db8::/32 -;; Wildcard hosts: *.mydomain.com, 192.168.100.* -;; Since 1.15.7. Default to * for 1.15.x, external for 1.16 and later -;ALLOWED_HOST_LIST = external -;; ;; Allow insecure certification ;SKIP_TLS_VERIFY = false ;; @@ -2997,8 +3003,16 @@ LEVEL = Info ;; Comma-separated list of workflow directories, the first one to exist ;; in a repo is used to find Actions workflow files ;WORKFLOW_DIRS = .gitea/workflows,.github/workflows +;; Comma-separated list of scoped workflow directories in a source repository, the first one to exist is used. +;; Files here are picked up only when the repo is registered as a scoped-workflow source; in any other repo they neither run repo-level nor scope-level. +;; Must not overlap with WORKFLOW_DIRS. Leave empty so no directory is scanned; no scoped workflows are found or run. +;SCOPED_WORKFLOW_DIRS = .gitea/scoped_workflows ;; Maximum number of attempts a single workflow run can have. Default value is 50. ;MAX_RERUN_ATTEMPTS = 50 +;; Maximum number of runners that may run the task-assignment query concurrently, per Gitea instance. +;; Caps this instance's DB load when many runners poll at once; excess runners retry on their next poll. +;; In a multi-instance deployment the cluster-wide limit is this value times the number of instances. Default value is 16. +;MAX_CONCURRENT_TASK_PICKS = 16 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/community-governance.md b/docs/community-governance.md index b721a2e8bd..da252a8e03 100644 --- a/docs/community-governance.md +++ b/docs/community-governance.md @@ -95,8 +95,15 @@ However, if there are no objections from maintainers, the PR can be merged with ### 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. +Usually the Pull Request description and commit message body should not be empty, unless the title is already clear enough or the description would be a copy of the comments in code. -The final commit message should not hedge: replace phrases like `hopefully, won't happen anymore` with definite wording. +The final commit message: + +- should match the code changes. +- should only keep true co-authors, false-positive co-authors should be removed. +- should not hedge: replace phrases like `hopefully, won't happen anymore` with definite wording. +- should not contain hidden information like `` or extra information after the description's divider `----`. +- should not contain unrelated contents (e.g.: Release Notes, Configuration, etc.) from a Renovate update PR. #### PR Co-authors @@ -158,9 +165,16 @@ Any account with write access (including bots and TOC members) **must** use [2FA Mergers are the maintainers who carry out the final merge of approved PRs. Their responsibilities, described throughout this guide, are: - Merging PRs from the [merge queue](#getting-prs-merged) in order, once a PR has `lgtm/done`, no open discussions, and no merge conflicts. -- Rewriting the PR title and summary so the squash [commit message](#commit-messages) is clear, removing false-positive co-authors while keeping every true co-author. +- Rewriting the PR title and description prior to the merge, making the [commit message](#commit-messages) clear.\ + In particular, mergers should edit the PR description.\ + Mergers should **not** edit the actual commit message except to remove unnecessary information. Because of that, even if users are looking at the PR, they can understand what changed. - Assigning the correct labels (including `type/…`) needed for changelog and backport decisions. - Agreeing, together with the owners, on when a release is ready (see [release management](release-management.md)). +- Merging a PR also means the PR looks good to the merger and is approved by the merger. + +If a merger violates these merge guides more than 3 times in the past 365 days +(e.g.: merge with unresolved reviews without TOC decision to ignore the review, merge with garbage commit messages), +they may lose their merging privileges for at least three months. #### Becoming a merger @@ -200,20 +214,20 @@ random.seed("Gitea TOC Election") random.choice([, , ...]) ``` -The result of this script needs then to be published in the TOC election issue to ensure transparency of the process. +The result of this script needs then to be published in the TOC election issue to ensure transparency of the process. ### Current TOC members - 2026-06-14 ~ 2026-12-31 - Company - - [Jason Song](https://gitea.com/wolfogre) + - [Yu Tian](https://gitea.com/Zettat123) - [Lunny Xiao](https://gitea.com/lunny) - [Matti Ranta](https://gitea.com/techknowlogick) - Community - [bircni](https://gitea.com/bircni) - [delvh](https://gitea.com/delvh) - [TheFox0x7](https://gitea.com/TheFox0x7) - + ### Previous TOC/owners members @@ -227,7 +241,7 @@ Here's the history of the owners and the time they served: - [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, 2025 - [John Olheiser](https://gitea.com/jolheiser) - 2023, 2024 -- [Jason Song](https://gitea.com/wolfogre) - 2023 +- [Jason Song](https://gitea.com/wolfogre) - 2023, 2025 ## Governance Compensation diff --git a/eslint.config.ts b/eslint.config.ts index a31b6c1fc7..707bde3b45 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,7 +1,4 @@ -import arrayFunc from 'eslint-plugin-array-func'; import comments from '@eslint-community/eslint-plugin-eslint-comments'; -import deMorgan from 'eslint-plugin-de-morgan'; -import github from 'eslint-plugin-github'; import globals from 'globals'; import importPlugin from 'eslint-plugin-import-x'; import playwright from 'eslint-plugin-playwright'; @@ -16,7 +13,6 @@ import vue from 'eslint-plugin-vue'; import vueScopedCss from 'eslint-plugin-vue-scoped-css'; import wc from 'eslint-plugin-wc'; import {defineConfig, globalIgnores} from 'eslint/config'; -import type {ESLint} from 'eslint'; import unescapedHtmlLiteral from './tools/eslint-rules/unescaped-html-literal.ts'; @@ -65,14 +61,11 @@ export default defineConfig([ '@eslint-community/eslint-comments': comments, '@stylistic': stylistic, '@typescript-eslint': typescriptPlugin.plugin, - 'array-func': arrayFunc, - 'de-morgan': deMorgan, 'gitea': {rules: {'unescaped-html-literal': unescapedHtmlLiteral}}, - 'import-x': importPlugin as unknown as ESLint.Plugin, // https://github.com/un-ts/eslint-plugin-import-x/issues/203 + 'import-x': importPlugin, regexp, sonarjs, unicorn, - github, wc, }, settings: { @@ -86,7 +79,6 @@ export default defineConfig([ '@eslint-community/eslint-comments/no-duplicate-disable': [2], '@eslint-community/eslint-comments/no-restricted-disable': [0], '@eslint-community/eslint-comments/no-unlimited-disable': [2], - '@eslint-community/eslint-comments/no-unused-disable': [2], '@eslint-community/eslint-comments/no-unused-enable': [2], '@eslint-community/eslint-comments/no-use': [0], '@eslint-community/eslint-comments/require-description': [0], @@ -193,7 +185,6 @@ export default defineConfig([ '@typescript-eslint/no-duplicate-type-constituents': [2, {ignoreUnions: true}], '@typescript-eslint/no-dynamic-delete': [0], '@typescript-eslint/no-empty-function': [0], - '@typescript-eslint/no-empty-interface': [0], '@typescript-eslint/no-empty-object-type': [2], '@typescript-eslint/no-explicit-any': [0], '@typescript-eslint/no-extra-non-null-assertion': [2], @@ -206,7 +197,6 @@ export default defineConfig([ '@typescript-eslint/no-invalid-this': [0], '@typescript-eslint/no-invalid-void-type': [0], '@typescript-eslint/no-loop-func': [0], - '@typescript-eslint/no-loss-of-precision': [0], '@typescript-eslint/no-magic-numbers': [0], '@typescript-eslint/no-meaningless-void-operator': [0], '@typescript-eslint/no-misused-new': [2], @@ -235,7 +225,7 @@ export default defineConfig([ '@typescript-eslint/no-unsafe-assignment': [0], '@typescript-eslint/no-unsafe-call': [0], '@typescript-eslint/no-unsafe-declaration-merging': [2], - '@typescript-eslint/no-unsafe-enum-comparison': [2], + '@typescript-eslint/no-unsafe-enum-comparison': [0], '@typescript-eslint/no-unsafe-function-type': [2], '@typescript-eslint/no-unsafe-member-access': [0], '@typescript-eslint/no-unsafe-return': [0], @@ -279,17 +269,10 @@ export default defineConfig([ '@typescript-eslint/strict-void-return': [0], '@typescript-eslint/switch-exhaustiveness-check': [0], '@typescript-eslint/triple-slash-reference': [2], - '@typescript-eslint/typedef': [0], '@typescript-eslint/unbound-method': [0], // too many false-positives '@typescript-eslint/unified-signatures': [2], 'accessor-pairs': [2], 'array-callback-return': [2, {checkForEach: true}], - 'array-func/avoid-reverse': [2], - 'array-func/from-map': [2], - 'array-func/no-unnecessary-this-arg': [2], - 'array-func/prefer-array-from': [2], - 'array-func/prefer-flat-map': [0], // handled by unicorn/prefer-array-flat-map - 'array-func/prefer-flat': [0], // handled by unicorn/prefer-array-flat 'arrow-body-style': [0], 'block-scoped-var': [2], 'camelcase': [0], @@ -300,8 +283,6 @@ 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], @@ -312,32 +293,9 @@ export default defineConfig([ 'func-names': [0], 'func-style': [0], 'getter-return': [2], - 'github/a11y-aria-label-is-well-formatted': [0], - 'github/a11y-no-title-attribute': [0], - 'github/a11y-no-visually-hidden-interactive-element': [0], - 'github/a11y-role-supports-aria-props': [0], - 'github/a11y-svg-has-accessible-name': [0], - 'github/array-foreach': [0], - 'github/async-currenttarget': [2], - 'github/async-preventdefault': [0], // https://github.com/github/eslint-plugin-github/issues/599 - 'github/authenticity-token': [0], - 'github/get-attribute': [0], - 'github/js-class-name': [0], - 'github/no-blur': [0], - 'github/no-d-none': [0], - 'github/no-dataset': [2], - 'github/no-dynamic-script-tag': [2], - 'github/no-implicit-buggy-globals': [2], - 'github/no-inner-html': [0], - 'github/no-innerText': [2], - 'github/no-then': [2], - 'github/no-useless-passive': [2], - 'github/prefer-observers': [0], - 'github/require-passive-events': [2], - 'gitea/unescaped-html-literal': [2], 'grouped-accessor-pairs': [2], 'guard-for-in': [0], - 'id-blacklist': [0], + 'id-denylist': [0], 'id-length': [0], 'id-match': [0], 'import-x/consistent-type-specifier-style': [0], @@ -384,7 +342,6 @@ export default defineConfig([ 'import-x/prefer-default-export': [0], 'import-x/unambiguous': [0], 'init-declarations': [0], - 'line-comment-position': [0], 'logical-assignment-operators': [0], 'max-classes-per-file': [0], 'max-depth': [0], @@ -393,14 +350,12 @@ export default defineConfig([ 'max-nested-callbacks': [0], 'max-params': [0], 'max-statements': [0], - 'multiline-comment-style': [0], 'new-cap': [0], 'no-alert': [0], 'no-array-constructor': [0], // handled by @typescript-eslint/no-array-constructor 'no-async-promise-executor': [0], 'no-await-in-loop': [0], 'no-bitwise': [0], - 'no-buffer-constructor': [0], 'no-caller': [2], 'no-case-declarations': [2], 'no-class-assign': [2], @@ -557,12 +512,11 @@ export default defineConfig([ 'no-nested-ternary': [0], 'no-new-func': [0], // handled by @typescript-eslint/no-implied-eval 'no-new-native-nonconstructor': [2], - 'no-new-object': [2], - 'no-new-symbol': [0], // handled by no-new-native-nonconstructor 'no-new-wrappers': [2], 'no-new': [0], 'no-nonoctal-decimal-escape': [2], 'no-obj-calls': [2], + 'no-object-constructor': [2], 'no-octal-escape': [2], 'no-octal': [2], 'no-param-reassign': [0], @@ -622,10 +576,8 @@ export default defineConfig([ 'no-warning-comments': [0], 'no-with': [0], // handled by no-restricted-syntax 'object-shorthand': [2, 'always'], - 'one-var-declaration-per-line': [0], 'one-var': [0], 'operator-assignment': [2, 'always'], - 'operator-linebreak': [0], // handled by @stylistic/operator-linebreak 'prefer-arrow-callback': [2, {allowNamedFunctions: true, allowUnboundThis: true}], 'prefer-const': [2, {destructuring: 'all', ignoreReadBeforeAssign: true}], 'prefer-destructuring': [0], @@ -761,139 +713,331 @@ export default defineConfig([ 'strict': [0], 'symbol-description': [2], 'unicode-bom': [2, 'never'], - 'unicorn/better-regex': [0], + 'unicorn/better-dom-traversing': [2], 'unicorn/catch-error-name': [0], + 'unicorn/class-reference-in-static-methods': [2], + 'unicorn/comment-content': [0], + 'unicorn/consistent-assert': [0], + 'unicorn/consistent-boolean-name': [0], + 'unicorn/consistent-class-member-order': [0], + 'unicorn/consistent-compound-words': [0], // too opinionated + 'unicorn/consistent-conditional-object-spread': [2], + 'unicorn/consistent-date-clone': [2], 'unicorn/consistent-destructuring': [2], 'unicorn/consistent-empty-array-spread': [2], - 'unicorn/consistent-template-literal-escape': [2], 'unicorn/consistent-existence-index-check': [0], + 'unicorn/consistent-export-decorator-position': [2], 'unicorn/consistent-function-scoping': [0], + 'unicorn/consistent-function-style': [2], + 'unicorn/consistent-json-file-read': [2], + 'unicorn/consistent-optional-chaining': [2], + 'unicorn/consistent-template-literal-escape': [2], + 'unicorn/consistent-tuple-labels': [2], 'unicorn/custom-error-definition': [0], + 'unicorn/default-export-style': [2], + 'unicorn/dom-node-dataset': [2, {preferAttributes: true}], 'unicorn/empty-brace-spaces': [2], 'unicorn/error-message': [0], 'unicorn/escape-case': [0], 'unicorn/expiring-todo-comments': [0], 'unicorn/explicit-length-check': [0], + 'unicorn/explicit-timer-delay': [2], 'unicorn/filename-case': [0], - 'unicorn/import-index': [0], + 'unicorn/id-match': [2], 'unicorn/import-style': [0], 'unicorn/isolated-functions': [2, {functions: []}], + 'unicorn/logical-assignment-operators': [0], + 'unicorn/max-nested-calls': [0], + 'unicorn/name-replacements': [0], 'unicorn/new-for-builtins': [2], 'unicorn/no-abusive-eslint-disable': [0], + 'unicorn/no-accessor-recursion': [2], + 'unicorn/no-accidental-bitwise-operator': [2], 'unicorn/no-anonymous-default-export': [0], 'unicorn/no-array-callback-reference': [0], - 'unicorn/no-array-for-each': [2], + 'unicorn/no-array-concat-in-loop': [2], + 'unicorn/no-array-fill-with-reference-type': [2], + 'unicorn/no-array-from-fill': [2], + 'unicorn/no-array-front-mutation': [0], 'unicorn/no-array-method-this-argument': [2], - 'unicorn/no-array-push-push': [2], 'unicorn/no-array-reduce': [2], + 'unicorn/no-array-reverse': [0], + 'unicorn/no-array-sort': [0], + 'unicorn/no-array-sort-for-min-max': [2], + 'unicorn/no-array-splice': [0], + 'unicorn/no-asterisk-prefix-in-documentation-comments': [0], + 'unicorn/no-async-promise-finally': [2], 'unicorn/no-await-expression-member': [0], 'unicorn/no-await-in-promise-methods': [2], + 'unicorn/no-blob-to-file': [2], + 'unicorn/no-boolean-sort-comparator': [2], + 'unicorn/no-break-in-nested-loop': [0], + 'unicorn/no-canvas-to-image': [2], + 'unicorn/no-chained-comparison': [2], + 'unicorn/no-collection-bracket-access': [2], + 'unicorn/no-computed-property-existence-check': [0], + 'unicorn/no-confusing-array-splice': [2], + 'unicorn/no-confusing-array-with': [2], 'unicorn/no-console-spaces': [0], + 'unicorn/no-constant-zero-expression': [2], + 'unicorn/no-declarations-before-early-exit': [0], 'unicorn/no-document-cookie': [2], + 'unicorn/no-double-comparison': [2], + 'unicorn/no-duplicate-if-branches': [2], + 'unicorn/no-duplicate-logical-operands': [2], + 'unicorn/no-duplicate-loops': [0], + 'unicorn/no-duplicate-set-values': [2], 'unicorn/no-empty-file': [2], + 'unicorn/no-error-property-assignment': [2], + 'unicorn/no-exports-in-scripts': [2], + 'unicorn/no-for-each': [2], 'unicorn/no-for-loop': [0], - 'unicorn/no-hex-escape': [0], + 'unicorn/no-global-object-property-assignment': [0], 'unicorn/no-immediate-mutation': [0], - 'unicorn/no-instanceof-array': [0], + 'unicorn/no-impossible-length-comparison': [2], + 'unicorn/no-incorrect-query-selector': [2], + 'unicorn/no-incorrect-template-string-interpolation': [0], + 'unicorn/no-instanceof-builtins': [2], + 'unicorn/no-invalid-argument-count': [0], + 'unicorn/no-invalid-character-comparison': [2], 'unicorn/no-invalid-fetch-options': [2], + 'unicorn/no-invalid-file-input-accept': [2], 'unicorn/no-invalid-remove-event-listener': [2], + 'unicorn/no-invalid-well-known-symbol-methods': [2], 'unicorn/no-keyword-prefix': [0], - 'unicorn/no-length-as-slice-end': [2], + 'unicorn/no-late-current-target-access': [2], + 'unicorn/no-late-event-control': [2], 'unicorn/no-lonely-if': [2], + 'unicorn/no-loop-iterable-mutation': [2], 'unicorn/no-magic-array-flat-depth': [0], + 'unicorn/no-manually-wrapped-comments': [0], // too opinionated + 'unicorn/no-mismatched-map-key': [2], + 'unicorn/no-misrefactored-assignment': [2], + 'unicorn/no-named-default': [2], + 'unicorn/no-negated-array-predicate': [2], + 'unicorn/no-negated-comparison': [2], 'unicorn/no-negated-condition': [0], 'unicorn/no-negation-in-equality-check': [2], 'unicorn/no-nested-ternary': [0], 'unicorn/no-new-array': [0], 'unicorn/no-new-buffer': [0], + 'unicorn/no-non-function-verb-prefix': [0], + 'unicorn/no-nonstandard-builtin-properties': [2], 'unicorn/no-null': [0], 'unicorn/no-object-as-default-parameter': [0], + 'unicorn/no-object-methods-with-collections': [2], + 'unicorn/no-optional-chaining-on-undeclared-variable': [2], 'unicorn/no-process-exit': [0], + 'unicorn/no-redundant-comparison': [2], + 'unicorn/no-return-array-push': [2], + 'unicorn/no-selector-as-dom-name': [2], 'unicorn/no-single-promise-in-promise-methods': [2], 'unicorn/no-static-only-class': [2], + 'unicorn/no-subtraction-comparison': [2], 'unicorn/no-thenable': [2], 'unicorn/no-this-assignment': [2], + 'unicorn/no-this-outside-of-class': [0], // gitea uses `this` in non-class functions + 'unicorn/no-top-level-assignment-in-function': [0], + 'unicorn/no-top-level-side-effects': [0], 'unicorn/no-typeof-undefined': [2], + 'unicorn/no-uncalled-method': [2], + 'unicorn/no-undeclared-class-members': [2], + 'unicorn/no-unnecessary-array-flat-depth': [2], + 'unicorn/no-unnecessary-array-flat-map': [2], + 'unicorn/no-unnecessary-array-splice-count': [2], 'unicorn/no-unnecessary-await': [2], + 'unicorn/no-unnecessary-boolean-comparison': [2], + 'unicorn/no-unnecessary-fetch-options': [0], + 'unicorn/no-unnecessary-global-this': [0], + 'unicorn/no-unnecessary-nested-ternary': [2], 'unicorn/no-unnecessary-polyfills': [2], + 'unicorn/no-unnecessary-slice-end': [2], + 'unicorn/no-unnecessary-splice': [2], 'unicorn/no-unreadable-array-destructuring': [0], + 'unicorn/no-unreadable-for-of-expression': [0], 'unicorn/no-unreadable-iife': [0], + 'unicorn/no-unreadable-new-expression': [0], + 'unicorn/no-unreadable-object-destructuring': [0], + 'unicorn/no-unsafe-buffer-conversion': [2], + 'unicorn/no-unsafe-dom-html': [0], + 'unicorn/no-unsafe-promise-all-settled-values': [2], + 'unicorn/no-unsafe-property-key': [0], + 'unicorn/no-unsafe-string-replacement': [0], + 'unicorn/no-unused-array-method-return': [2], 'unicorn/no-unused-properties': [2], + 'unicorn/no-useless-boolean-cast': [2], + 'unicorn/no-useless-coercion': [2], 'unicorn/no-useless-collection-argument': [2], + 'unicorn/no-useless-compound-assignment': [2], + 'unicorn/no-useless-concat': [2], + 'unicorn/no-useless-continue': [2], + 'unicorn/no-useless-delete-check': [2], + 'unicorn/no-useless-else': [0], + 'unicorn/no-useless-error-capture-stack-trace': [2], 'unicorn/no-useless-fallback-in-spread': [2], 'unicorn/no-useless-iterator-to-array': [2], 'unicorn/no-useless-length-check': [2], + 'unicorn/no-useless-logical-operand': [2], + 'unicorn/no-useless-override': [2], 'unicorn/no-useless-promise-resolve-reject': [2], + 'unicorn/no-useless-recursion': [0], 'unicorn/no-useless-spread': [2], 'unicorn/no-useless-switch-case': [2], + 'unicorn/no-useless-template-literals': [2], 'unicorn/no-useless-undefined': [0], + 'unicorn/no-xor-as-exponentiation': [2], 'unicorn/no-zero-fractions': [2], 'unicorn/number-literal-case': [0], 'unicorn/numeric-separators-style': [0], + 'unicorn/operator-assignment': [2], + 'unicorn/prefer-abort-signal-any': [2], + 'unicorn/prefer-abort-signal-timeout': [2], 'unicorn/prefer-add-event-listener': [2], + 'unicorn/prefer-add-event-listener-options': [2], + 'unicorn/prefer-aggregate-error': [2], 'unicorn/prefer-array-find': [0], // handled by @typescript-eslint/prefer-find 'unicorn/prefer-array-flat': [2], 'unicorn/prefer-array-flat-map': [2], + 'unicorn/prefer-array-from-async': [2], + 'unicorn/prefer-array-from-map': [2], + 'unicorn/prefer-array-from-range': [2], 'unicorn/prefer-array-index-of': [2], + 'unicorn/prefer-array-iterable-methods': [2], + 'unicorn/prefer-array-last-methods': [2], + 'unicorn/prefer-array-slice': [2], 'unicorn/prefer-array-some': [2], 'unicorn/prefer-at': [0], + 'unicorn/prefer-await': [2], + 'unicorn/prefer-bigint-literals': [2], 'unicorn/prefer-blob-reading-methods': [2], + 'unicorn/prefer-block-statement-over-iife': [2], + 'unicorn/prefer-boolean-return': [2], + 'unicorn/prefer-class-fields': [2], + 'unicorn/prefer-classlist-toggle': [2], 'unicorn/prefer-code-point': [0], + 'unicorn/prefer-continue': [0], 'unicorn/prefer-date-now': [2], 'unicorn/prefer-default-parameters': [0], + 'unicorn/prefer-direct-iteration': [2], + 'unicorn/prefer-dispose': [2], 'unicorn/prefer-dom-node-append': [2], - 'unicorn/prefer-dom-node-dataset': [0], + 'unicorn/prefer-dom-node-html-methods': [0], 'unicorn/prefer-dom-node-remove': [2], + 'unicorn/prefer-dom-node-replace-children': [2], 'unicorn/prefer-dom-node-text-content': [2], + 'unicorn/prefer-early-return': [0], + 'unicorn/prefer-else-if': [2], + 'unicorn/prefer-error-is-error': [0], 'unicorn/prefer-event-target': [2], 'unicorn/prefer-export-from': [0], + 'unicorn/prefer-flat-math-min-max': [2], + 'unicorn/prefer-get-or-insert-computed': [2], + 'unicorn/prefer-global-number-constants': [2], 'unicorn/prefer-global-this': [0], + 'unicorn/prefer-group-by': [2], + 'unicorn/prefer-has-check': [2], + 'unicorn/prefer-hoisting-branch-code': [2], + 'unicorn/prefer-https': [0], // false-positives on namespace and schema URIs + 'unicorn/prefer-identifier-import-export-specifiers': [2], + 'unicorn/prefer-import-meta-properties': [2], 'unicorn/prefer-includes': [0], // handled by @typescript-eslint/prefer-includes - 'unicorn/prefer-json-parse-buffer': [0], + 'unicorn/prefer-includes-over-repeated-comparisons': [0], // too opinionated + 'unicorn/prefer-iterable-in-constructor': [2], + 'unicorn/prefer-iterator-concat': [0], // too opinionated + 'unicorn/prefer-iterator-helpers': [2], + 'unicorn/prefer-iterator-to-array': [2], + 'unicorn/prefer-iterator-to-array-at-end': [2], 'unicorn/prefer-keyboard-event-key': [2], + 'unicorn/prefer-location-assign': [2], 'unicorn/prefer-logical-operator-over-ternary': [2], + 'unicorn/prefer-map-from-entries': [0], + 'unicorn/prefer-math-abs': [2], + 'unicorn/prefer-math-constants': [2], 'unicorn/prefer-math-min-max': [2], 'unicorn/prefer-math-trunc': [2], + 'unicorn/prefer-minimal-ternary': [0], 'unicorn/prefer-modern-dom-apis': [0], 'unicorn/prefer-modern-math-apis': [2], 'unicorn/prefer-module': [2], 'unicorn/prefer-native-coercion-functions': [2], 'unicorn/prefer-negative-index': [2], 'unicorn/prefer-node-protocol': [2], + 'unicorn/prefer-number-coercion': [0], + 'unicorn/prefer-number-is-safe-integer': [0], 'unicorn/prefer-number-properties': [0], + 'unicorn/prefer-object-define-properties': [2], + 'unicorn/prefer-object-destructuring-defaults': [2], 'unicorn/prefer-object-from-entries': [2], - 'unicorn/prefer-object-has-own': [0], + 'unicorn/prefer-object-iterable-methods': [2], + 'unicorn/prefer-observer-apis': [2], 'unicorn/prefer-optional-catch-binding': [2], + 'unicorn/prefer-path2d': [2], + 'unicorn/prefer-private-class-fields': [0], + 'unicorn/prefer-promise-try': [2], + 'unicorn/prefer-promise-with-resolvers': [2], 'unicorn/prefer-prototype-methods': [0], 'unicorn/prefer-query-selector': [2], + 'unicorn/prefer-queue-microtask': [2], 'unicorn/prefer-reflect-apply': [0], + 'unicorn/prefer-regexp-escape': [0], 'unicorn/prefer-regexp-test': [2], 'unicorn/prefer-response-static-json': [2], + 'unicorn/prefer-scoped-selector': [0], 'unicorn/prefer-set-has': [0], + 'unicorn/prefer-set-methods': [0], 'unicorn/prefer-set-size': [2], + 'unicorn/prefer-short-arrow-method': [2], 'unicorn/prefer-simple-condition-first': [0], + 'unicorn/prefer-simple-sort-comparator': [2], + 'unicorn/prefer-simplified-conditions': [2], + 'unicorn/prefer-single-array-predicate': [2], + 'unicorn/prefer-single-call': [2], + 'unicorn/prefer-single-object-destructuring': [2], + 'unicorn/prefer-single-replace': [2], + 'unicorn/prefer-smaller-scope': [2], + 'unicorn/prefer-split-limit': [0], // too opinionated 'unicorn/prefer-spread': [0], + 'unicorn/prefer-string-match-all': [2], + 'unicorn/prefer-string-pad-start-end': [2], 'unicorn/prefer-string-raw': [0], + 'unicorn/prefer-string-repeat': [2], 'unicorn/prefer-string-replace-all': [0], 'unicorn/prefer-string-slice': [0], 'unicorn/prefer-string-starts-ends-with': [0], // handled by @typescript-eslint/prefer-string-starts-ends-with 'unicorn/prefer-string-trim-start-end': [2], 'unicorn/prefer-structured-clone': [2], 'unicorn/prefer-switch': [0], + 'unicorn/prefer-temporal': [0], 'unicorn/prefer-ternary': [0], + 'unicorn/prefer-toggle-attribute': [2], 'unicorn/prefer-top-level-await': [0], 'unicorn/prefer-type-error': [0], + 'unicorn/prefer-type-literal-last': [0], + 'unicorn/prefer-uint8array-base64': [0], + 'unicorn/prefer-unary-minus': [2], + 'unicorn/prefer-unicode-code-point-escapes': [0], + 'unicorn/prefer-url-can-parse': [2], + 'unicorn/prefer-url-href': [2], + 'unicorn/prefer-url-search-parameters': [2], + 'unicorn/prefer-while-loop-condition': [2], 'unicorn/prevent-abbreviations': [0], 'unicorn/relative-url-style': [2], 'unicorn/require-array-join-separator': [2], + 'unicorn/require-array-sort-compare': [0], + 'unicorn/require-css-escape': [2], + 'unicorn/require-module-attributes': [2], + 'unicorn/require-module-specifiers': [0], 'unicorn/require-number-to-fixed-digits-argument': [2], + 'unicorn/require-passive-events': [2], 'unicorn/require-post-message-target-origin': [0], + 'unicorn/require-proxy-trap-boolean-return': [2], 'unicorn/string-content': [0], 'unicorn/switch-case-braces': [0], 'unicorn/switch-case-break-position': [2], 'unicorn/template-indent': [2], 'unicorn/text-encoding-identifier-case': [0], 'unicorn/throw-new-error': [2], + 'unicorn/try-complexity': [0], 'use-isnan': [2], 'valid-typeof': [2, {requireStringLiterals: true}], 'vars-on-top': [0], @@ -956,6 +1100,7 @@ export default defineConfig([ languageOptions: {globals: globals.vitest}, rules: { 'gitea/unescaped-html-literal': [0], + 'unicorn/no-error-property-assignment': [0], 'vitest/consistent-test-filename': [0], 'vitest/consistent-test-it': [0], 'vitest/expect-expect': [0], @@ -967,7 +1112,6 @@ export default defineConfig([ 'vitest/no-conditional-in-test': [0], 'vitest/no-conditional-tests': [0], 'vitest/no-disabled-tests': [0], - 'vitest/no-done-callback': [0], 'vitest/no-duplicate-hooks': [0], 'vitest/no-focused-tests': [2], 'vitest/no-hooks': [0], diff --git a/flake.lock b/flake.lock index 839eaed572..6492c26f94 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1776877367, - "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=", + "lastModified": 1782723713, + "narHash": "sha256-oPXCU/SSUokcGaJREHibG1CBX3+s/W7orDWQOZDsEeQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57", + "rev": "b5aa0fbd538984f6e3d201be0005b4463d8b09f8", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index ca88b581a2..9cf51ab0bb 100644 --- a/flake.nix +++ b/flake.nix @@ -34,7 +34,7 @@ # only bump toolchain versions here go = pkgs.go_1_26; - nodejs = pkgs.nodejs_24; + nodejs = pkgs.nodejs_26; python3 = pkgs.python314; pnpm = pkgs.pnpm_10; diff --git a/go.mod b/go.mod index 9d23a99410..24346b3bc6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module gitea.dev -go 1.26.4 +go 1.26.5 require ( codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 @@ -9,11 +9,11 @@ require ( 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/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6 gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 gitea.dev/actions-proto-go v0.6.0 - gitea.dev/sdk v1.1.0 + gitea.dev/sdk v1.2.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.22.0 @@ -23,17 +23,17 @@ require ( github.com/ProtonMail/go-crypto v1.4.1 github.com/PuerkitoBio/goquery v1.12.0 github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0 - github.com/alecthomas/chroma/v2 v2.26.1 - github.com/aws/aws-sdk-go-v2/credentials v1.19.24 - github.com/aws/aws-sdk-go-v2/service/codecommit v1.34.4 + github.com/alecthomas/chroma/v2 v2.27.0 + github.com/aws/aws-sdk-go-v2/credentials v1.19.28 + github.com/aws/aws-sdk-go-v2/service/codecommit v1.35.0 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blevesearch/bleve/v2 v2.6.0 github.com/bohde/codel v0.2.0 - github.com/buildkite/terminal-to-html/v3 v3.16.8 - github.com/caddyserver/certmagic v0.25.3 + github.com/buildkite/terminal-to-html/v3 v3.17.1 + github.com/caddyserver/certmagic v0.25.4 github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20260309112543-12416315a635 github.com/chi-middleware/proxy v1.1.1 - github.com/dlclark/regexp2/v2 v2.2.1 + github.com/dlclark/regexp2/v2 v2.2.2 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,20 +44,20 @@ require ( github.com/fsnotify/fsnotify v1.10.1 github.com/getkin/kin-openapi v0.140.0 github.com/gliderlabs/ssh v0.3.8 - github.com/go-chi/chi/v5 v5.3.0 + github.com/go-chi/chi/v5 v5.3.1 github.com/go-chi/cors v1.2.2 github.com/go-co-op/gocron/v2 v2.21.2 github.com/go-enry/go-enry/v2 v2.9.6 github.com/go-git/go-billy/v5 v5.9.0 github.com/go-git/go-git/v5 v5.19.1 github.com/go-ldap/ldap/v3 v3.4.13 - github.com/go-redsync/redsync/v4 v4.16.0 + github.com/go-redsync/redsync/v4 v4.17.0 github.com/go-sql-driver/mysql v1.10.0 github.com/go-webauthn/webauthn v0.17.4 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/v88 v88.0.0 + github.com/google/go-github/v89 v89.0.0 github.com/google/licenseclassifier/v2 v2.0.0 github.com/google/pprof v0.0.0-20260604005048-7023385849c0 github.com/google/uuid v1.6.0 @@ -68,17 +68,17 @@ require ( github.com/huandu/xstrings v1.5.0 github.com/jhillyerd/enmime/v2 v2.4.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/klauspost/compress v1.18.6 - github.com/klauspost/cpuid/v2 v2.3.0 + github.com/klauspost/compress v1.19.0 + github.com/klauspost/cpuid/v2 v2.4.0 github.com/lib/pq v1.12.3 github.com/markbates/goth v1.82.0 github.com/mattn/go-isatty v0.0.22 - github.com/mattn/go-sqlite3 v1.14.45 + github.com/mattn/go-sqlite3 v1.14.47 github.com/meilisearch/meilisearch-go v0.36.3 github.com/mholt/archives v0.1.5 github.com/microcosm-cc/bluemonday v1.0.27 github.com/microsoft/go-mssqldb v1.10.0 - github.com/minio/minio-go/v7 v7.2.0 + github.com/minio/minio-go/v7 v7.2.1 github.com/msteinert/pam/v2 v2.1.0 github.com/niklasfasching/go-org v1.9.1 github.com/opencontainers/go-digest v1.0.0 @@ -86,7 +86,7 @@ require ( github.com/pquerna/otp v1.5.0 github.com/prometheus/client_golang v1.23.2 github.com/quasoft/websspi v1.1.2 - github.com/redis/go-redis/v9 v9.20.0 + github.com/redis/go-redis/v9 v9.21.0 github.com/robfig/cron/v3 v3.0.1 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/sassoftware/go-rpmutils v0.4.0 @@ -96,29 +96,29 @@ require ( github.com/tstranex/u2f v1.0.0 github.com/ulikunitz/xz v0.5.15 github.com/urfave/cli-docs/v3 v3.1.0 - github.com/urfave/cli/v3 v3.9.1 - github.com/wneessen/go-mail v0.7.3 + github.com/urfave/cli/v3 v3.10.1 + github.com/wneessen/go-mail v0.8.1 github.com/yohcop/openid-go v1.0.1 github.com/yuin/goldmark v1.8.2 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc - gitlab.com/gitlab-org/api/client-go/v2 v2.38.0 + gitlab.com/gitlab-org/api/client-go/v2 v2.46.0 go.yaml.in/yaml/v4 v4.0.0-rc.5 - golang.org/x/crypto v0.53.0 - golang.org/x/image v0.42.0 - golang.org/x/mod v0.37.0 - golang.org/x/net v0.56.0 + golang.org/x/crypto v0.54.0 + golang.org/x/image v0.44.0 + golang.org/x/mod v0.38.0 + golang.org/x/net v0.57.0 golang.org/x/oauth2 v0.36.0 - golang.org/x/sync v0.21.0 - golang.org/x/sys v0.46.0 - golang.org/x/text v0.38.0 - google.golang.org/grpc v1.81.1 + golang.org/x/sync v0.22.0 + golang.org/x/sys v0.47.0 + golang.org/x/text v0.40.0 + google.golang.org/grpc v1.82.0 google.golang.org/protobuf v1.36.11 gopkg.in/ini.v1 v1.67.3 - modernc.org/sqlite v1.52.0 + modernc.org/sqlite v1.53.0 mvdan.cc/xurls/v2 v2.6.0 strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab xorm.io/builder v0.3.13 - xorm.io/xorm v1.3.11 + xorm.io/xorm v1.4.1 ) require ( @@ -133,10 +133,10 @@ require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/andybalholm/cascadia v1.3.4 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect - github.com/aws/aws-sdk-go-v2 v1.42.0 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect - github.com/aws/smithy-go v1.27.2 // indirect + github.com/aws/aws-sdk-go-v2 v1.42.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 // indirect + github.com/aws/smithy-go v1.27.3 // 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.5 // indirect @@ -199,9 +199,7 @@ require ( github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect 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 @@ -269,11 +267,11 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.45.0 // indirect + golang.org/x/tools v0.47.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.73.0 // indirect + modernc.org/libc v1.73.4 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 1de4df2d2c..45c2492a34 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ 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= gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk= -gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e h1:4bugwPyGMLvblEm3pZ8fZProSPVxE4l0UXF2Kv6IJoY= -gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e/go.mod h1:KDvcfMUoXfATPHs2mbMoXFTXT45/FAFAS39waz9tPk0= +gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6 h1:YWzVGeC/8SZThrJS48ZmQYLkzssdeABxHPhbdnxPDIU= +gitea.com/go-chi/session v0.0.0-20260708011333-ebced8a7a2d6/go.mod h1:KDvcfMUoXfATPHs2mbMoXFTXT45/FAFAS39waz9tPk0= gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw= gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY= gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o= @@ -28,8 +28,8 @@ gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGq gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= gitea.dev/actions-proto-go v0.6.0 h1:gjllYQ5vmwlkqOeofTQu5qKTZpmf7kWsafoHvoPCSzY= gitea.dev/actions-proto-go v0.6.0/go.mod h1:p4RX+D9oqiEEzzkPMXscw2CmaGuYFPWFc6xIOmDNDqs= -gitea.dev/sdk v1.1.0 h1:wLlz03WkLEiXa2bQpO1JQBTlYf7tQI2neYtZK1kU+TE= -gitea.dev/sdk v1.1.0/go.mod h1:Zfl+EZXdsGGCLkryDfsmvYrQo6GKMl4U3BJA8Beu+cs= +gitea.dev/sdk v1.2.0 h1:avRtJl/nKCGispgSalo9czoZM9Rto1awnE0caNAoXGo= +gitea.dev/sdk v1.2.0/go.mod h1:rfh5oNdIK24cbCREwIn1tqWKQW+IICXFGWJyebuOAOE= 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= @@ -76,8 +76,8 @@ github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0/go.mod h1:I28hc9eaiqK github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -github.com/alecthomas/chroma/v2 v2.26.1 h1:2X21EdxGZNv5GF9mG5u+uzc02GCFyGxbcBm3Grd9A78= -github.com/alecthomas/chroma/v2 v2.26.1/go.mod h1:lxhRRa9H4hPmRLOOdYga4zkQIQjq3dtrrdwQeCfu78Y= +github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs= +github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8= github.com/alecthomas/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= @@ -92,18 +92,18 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= -github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= -github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I= -github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= -github.com/aws/aws-sdk-go-v2/service/codecommit v1.34.4 h1:Uu+wqrOXozYYvaxcNIqjFsMTjoIJIZDN3R0f70ZIjyQ= -github.com/aws/aws-sdk-go-v2/service/codecommit v1.34.4/go.mod h1:pYrBdL1tMTZO7PaKRsa1cTUB8HtQh3fFM3zJHGhTQcE= -github.com/aws/smithy-go v1.27.2 h1:y9NPmSE6am6LjEFPfqHqG/jJk7AauQvhCJONKh7kpzk= -github.com/aws/smithy-go v1.27.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek= +github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM= +github.com/aws/aws-sdk-go-v2/credentials v1.19.28 h1:zTXJSsNcoO91/mTXsZoYf0AK8dvNPiA58/VtyGXR+wM= +github.com/aws/aws-sdk-go-v2/credentials v1.19.28/go.mod h1:Kd9E0JzDBW/q1xbsHFrev/GnbAf5J0Ng8xoyc7HZ91Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30/go.mod h1:WueJeNDZvK1fMYEWJIkcivBfEzUkTpBhzlrUKKY8EuA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 h1:jn46zC9LdsVR/ZpMIJqMqb8hHv31BlLx3ulVqNspUOk= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30/go.mod h1:1hTMsAgbdS/AtUi4bw8+gUuh1pceo+eXRLfpSuSQj3M= +github.com/aws/aws-sdk-go-v2/service/codecommit v1.35.0 h1:w/iPWj+VObIbyo1WYN5010Bi61OkkqOiFZpNDAP1Rfc= +github.com/aws/aws-sdk-go-v2/service/codecommit v1.35.0/go.mod h1:uH156Pb0jnMXuCSD9irU0Qz6UNhYUF2qbowFqIwyLQ8= +github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY= +github.com/aws/smithy-go v1.27.3/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= @@ -185,10 +185,10 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 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.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A= -github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA= +github.com/buildkite/terminal-to-html/v3 v3.17.1 h1:gs9WhKUwxK27YHf3hijj4tG4wuDLfCANYbRPd5ltMdI= +github.com/buildkite/terminal-to-html/v3 v3.17.1/go.mod h1:0pv+Z75FC+NvIju2o+Cw+YHIDkueeudIoeyfVm8i8GQ= +github.com/caddyserver/certmagic v0.25.4 h1:8eIXh0HC3MsGnNo8One+BCxMGTbe5zb/oz+2KsxBFQg= +github.com/caddyserver/certmagic v0.25.4/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA= github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= @@ -240,8 +240,8 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0= -github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= +github.com/dlclark/regexp2/v2 v2.2.2 h1:MYWvNYw8okuqNhwTYO587EZMiDruVa2vhV6fsGpfya0= +github.com/dlclark/regexp2/v2 v2.2.2/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= @@ -285,8 +285,8 @@ github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1T github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM= -github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= +github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8= +github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-co-op/gocron/v2 v2.21.2 h1:bD8/YwkojYHgXFr3iEulL148KBdTbKVxUZzFKpXcdbY= @@ -319,8 +319,8 @@ github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOr 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.16.0 h1:bNcOzeHH9d3s6pghU9NJFMPrQa41f5Nx3L4YKr3BdEU= -github.com/go-redsync/redsync/v4 v4.16.0/go.mod h1:V4gagqgyASWBZuwx4xGzu72aZNb/6Mo05byUa3mVmKQ= +github.com/go-redsync/redsync/v4 v4.17.0 h1:FFJ+uxZs44y4Sq10//IFKic9T94AYl+u3Sog98AHzBo= +github.com/go-redsync/redsync/v4 v4.17.0/go.mod h1:CKVA6qwT07S/916i+Yd9h1/8YFQhCCpPYTQhvvYytJo= 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= @@ -376,8 +376,8 @@ 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/v88 v88.0.0 h1:dZA9IKkPK1eXZj4ypngnpRj5FwdpTv4whix2PrQMP7M= -github.com/google/go-github/v88 v88.0.0/go.mod h1:rufTDgn2N45wjhukLTyxmvc9nilSp3mr3Rgtt6b1MPw= +github.com/google/go-github/v89 v89.0.0 h1:35bEK5XoEcF3PZrlVbl9XN63f5BcJRA/UGkxeC9xPg0= +github.com/google/go-github/v89 v89.0.0/go.mod h1:QLcbU0ipeAqQuR5KSg8c2lql4Qk1EwJ2dWz/0rP4Nho= 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= @@ -413,17 +413,10 @@ 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.10.2 h1:HXu6Wu5klCH4ALn1fQHVI20cjEIa4wftavHIgbLA4Fo= -github.com/graph-gophers/graphql-go v1.10.2/go.mod h1:AsADheC4CCFwd8n1/QbkduTlHgYYMsRgtPihYVAlEsk= -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= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= @@ -471,12 +464,12 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C 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.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= -github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ= +github.com/klauspost/compress v1.19.0/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= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= @@ -514,8 +507,8 @@ github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/a github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4= github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk= -github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= +github.com/mattn/go-sqlite3 v1.14.47 h1:jOBI62gS7nKeZv+as1oGEy0+1qISgXwH/QBlR6KbfIo= +github.com/mattn/go-sqlite3 v1.14.47/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/meilisearch/meilisearch-go v0.36.3 h1:Yx1aTY5jDgtbStPVkhJTDoLnZTy5sejQSPyjfNMy6e4= github.com/meilisearch/meilisearch-go v0.36.3/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM= github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk= @@ -534,8 +527,8 @@ 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.2.0 h1:RCJM0R1XOsRs+A3x3UCaf3ZYbByDaLjFeAi+YCQEPhs= -github.com/minio/minio-go/v7 v7.2.0/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0= +github.com/minio/minio-go/v7 v7.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw= +github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0= github.com/minio/minlz v1.1.1 h1:OGmft1V6AnI/Wme332U6bhG54nxEan+VFgkD7lat4KM= github.com/minio/minlz v1.1.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -618,12 +611,12 @@ github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4Ul github.com/quasoft/websspi v1.1.2 h1:/mA4w0LxWlE3novvsoEL6BBA1WnjJATbjkh1kFrTidw= github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk= github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.20.0 h1:WnQYxLkgO2xiXTCJY0ldIiI8dNqCDlQAG+AtaH7a2a0= -github.com/redis/go-redis/v9 v9.20.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= -github.com/redis/rueidis v1.0.71 h1:pODtnAR5GAB7j4ekhldZ29HKOxe4Hph0GTDGk1ayEQY= -github.com/redis/rueidis v1.0.71/go.mod h1:lfdcZzJ1oKGKL37vh9fO3ymwt+0TdjkkUCJxbgpmcgQ= -github.com/redis/rueidis/rueidiscompat v1.0.71 h1:wNZ//kEjMZgBM0KCk7ncOX8KmAgROU2kDdDNpwheG4w= -github.com/redis/rueidis/rueidiscompat v1.0.71/go.mod h1:esmCLJvaRzZoKlgB82G1bY7Iky5TnO9Rz+NlhbEccFI= +github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E= +github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= +github.com/redis/rueidis v1.0.76 h1:RdDWuvlYBSp+bTrBvaXqJnNEL3VVzsnjo+0psPFgLc4= +github.com/redis/rueidis v1.0.76/go.mod h1:UsfHPSbomB6QAVMk4iiFkzRy0nh9o7scDGa+SitvBY4= +github.com/redis/rueidis/rueidiscompat v1.0.76 h1:7LikbiqCQqCsZXeZ+akgZMnjIV/J0VHih9PIX4gGZC4= +github.com/redis/rueidis/rueidiscompat v1.0.76/go.mod h1:UatQQLVj4QMIsZtpvRWY28qm6r2d72idhcS+C/RM+Zg= 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.12 h1:vQ4GeJN86C0QH+gTUQcs8McmK62OLT3kmakPMtEWYnY= @@ -709,11 +702,11 @@ github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw= github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to= -github.com/urfave/cli/v3 v3.9.1 h1:OLU13atWZ0M+a4xmyBuBNOLZsSRYXyPeMeNjOvgYP54= -github.com/urfave/cli/v3 v3.9.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY= +github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= -github.com/wneessen/go-mail v0.7.3 h1:g3DravXC5SMlVdboFrQA8Jx95A8sOzoBeS5F+vzNRK0= -github.com/wneessen/go-mail v0.7.3/go.mod h1:QGhBX0yNbc1J+Mkjcu7z2rpj4B4l+BmDY8gYznPC9sk= +github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pcM= +github.com/wneessen/go-mail v0.8.1/go.mod h1:dWZ61zadzCIyvB4y1/YzC5O7MrbbzBfPkARmbosdf8w= 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= @@ -740,8 +733,8 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= -gitlab.com/gitlab-org/api/client-go/v2 v2.38.0 h1:gZSMTTnLcUeY5mH4z3G6GEzbaBTOCUfBCAJXMRyuzEM= -gitlab.com/gitlab-org/api/client-go/v2 v2.38.0/go.mod h1:SKUbKSS59KPt6WeGNJoYF8HDaf/rFMUSITlftj/HkLg= +gitlab.com/gitlab-org/api/client-go/v2 v2.46.0 h1:t9QSeMck3kuHu9R6C7h8cYk4qBI4ZD4SQ0jspxKYs2E= +gitlab.com/gitlab-org/api/client-go/v2 v2.46.0/go.mod h1:1LK5UOowqo9apkL+zn7sM8t/hCzBV6F1b2RUIHL9+Oo= 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= @@ -776,12 +769,12 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= -golang.org/x/image v0.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY= -golang.org/x/image v0.42.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q= +golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= +golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -790,8 +783,8 @@ 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.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -807,8 +800,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 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.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= 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= @@ -822,8 +815,8 @@ 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.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.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= @@ -854,8 +847,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.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= @@ -865,8 +858,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 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.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -877,8 +870,8 @@ 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.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= 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= @@ -890,16 +883,16 @@ 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.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= 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/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg= google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= +google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= 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= @@ -940,8 +933,8 @@ modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc= modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.73.0 h1:Y/KmTxbIN5T3x+NFjYOzV/+Ha7wKClfIecmTCTuYlqQ= -modernc.org/libc v1.73.0/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8= +modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA= +modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -950,8 +943,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo= -modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM= +modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M= +modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s= 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= @@ -964,5 +957,5 @@ strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab h1:3IZDVyI 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= -xorm.io/xorm v1.3.11/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q= +xorm.io/xorm v1.4.1 h1:m7QlNd0eBGb31IV4Q/ow0Du83rtdC1CiwlvJZGvYde8= +xorm.io/xorm v1.4.1/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q= diff --git a/models/actions/run.go b/models/actions/run.go index bba6b4c34e..0d6c2bbf15 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "net/url" "strings" "time" @@ -21,6 +22,8 @@ import ( "gitea.dev/modules/timeutil" "gitea.dev/modules/util" webhook_module "gitea.dev/modules/webhook" + + "xorm.io/builder" ) // ActionRun represents a run of a workflow file @@ -48,6 +51,13 @@ type ActionRun struct { Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed RawConcurrency string // raw concurrency + // WorkflowRepoID/WorkflowCommitSHA record the (repo, commit) the run's workflow file content came from. + // Always filled (repo-level run = the repo itself; scoped run = the source repo). + WorkflowRepoID int64 `xorm:"NOT NULL DEFAULT 0"` + WorkflowCommitSHA string `xorm:"VARCHAR(64) NOT NULL DEFAULT ''"` + + IsScopedRun bool `xorm:"NOT NULL DEFAULT false"` // IsScopedRun explicitly classifies scoped runs. + // Started and Stopped are identical to the latest attempt after ActionRunAttempt was introduced. // When a rerun creates a new latest attempt, they are reset until the new attempt starts and stops. Started timeutil.TimeStamp @@ -86,7 +96,11 @@ func (run *ActionRun) WorkflowLink() string { if run.Repo == nil { return "" } - return fmt.Sprintf("%s/actions/?workflow=%s", run.Repo.Link(), run.WorkflowID) + // A scoped run's workflow is disambiguated by its source repo, so carry scoped_workflow_source_repo_id back to the run list + if run.IsScopedRun { + return fmt.Sprintf("%s/actions/?workflow=%s&scoped_workflow_source_repo_id=%d", run.Repo.Link(), url.QueryEscape(run.WorkflowID), run.WorkflowRepoID) + } + return fmt.Sprintf("%s/actions/?workflow=%s", run.Repo.Link(), url.QueryEscape(run.WorkflowID)) } // RefLink return the url of run's ref @@ -107,7 +121,7 @@ func (run *ActionRun) PrettyRef() string { return refName.ShortName() } -// RefTooltip return a tooltop of run's ref. For pull request, it's the title of the PR, otherwise it's the ShortName. +// RefTooltip return a tooltip of run's ref. For pull request, it's the title of the PR, otherwise it's the ShortName. func (run *ActionRun) RefTooltip() string { payload, err := run.GetPullRequestEventPayload() if err == nil && payload != nil && payload.PullRequest != nil { @@ -263,12 +277,14 @@ func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, er return &run, nil } +func GetRunsByRepoAndID(ctx context.Context, repoID int64, runIDs []int64) ([]*ActionRun, error) { + var runs []*ActionRun + err := db.GetEngine(ctx).In("id", runIDs).Where("repo_id=?", repoID).Find(&runs) + return runs, err +} + func GetRunByRepoAndIndex(ctx context.Context, repoID, runIndex int64) (*ActionRun, error) { - run := &ActionRun{ - RepoID: repoID, - Index: runIndex, - } - has, err := db.GetEngine(ctx).Get(run) + run, has, err := db.Get[ActionRun](ctx, builder.Eq{"repo_id": repoID, "`index`": runIndex}) if err != nil { return nil, err } else if !has { @@ -279,9 +295,7 @@ func GetRunByRepoAndIndex(ctx context.Context, repoID, runIndex int64) (*ActionR } func GetLatestRun(ctx context.Context, repoID int64) (*ActionRun, error) { - run := &ActionRun{ - RepoID: repoID, - } + run := &ActionRun{} has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).Desc("index").Get(run) if err != nil { return nil, err @@ -295,7 +309,10 @@ func GetWorkflowLatestRun(ctx context.Context, repoID int64, workflowFile, branc var run ActionRun q := db.GetEngine(ctx).Where("repo_id=?", repoID). And("ref = ?", branch). - And("workflow_id = ?", workflowFile) + And("workflow_id = ?", workflowFile). + // TODO: the badge only reflects the repo's own (repo-level) runs; a same-named scoped run must not leak in. + // Support a scoped-workflow badge later by making this source-aware. + And("is_scoped_run = ?", false) if event != "" { q.And("event = ?", event) } diff --git a/models/actions/run_job.go b/models/actions/run_job.go index df01546fd8..33696f7bee 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -108,6 +108,10 @@ type ActionRunJob struct { // ParentJobID scopes `Needs` resolution: name lookups happen only among rows sharing the same ParentJobID. 0 for top-level rows. ParentJobID int64 `xorm:"index NOT NULL DEFAULT 0"` + // ContinueOnError mirrors the job-level continue-on-error field from the workflow YAML. + // When true, a failure of this job does not fail the overall workflow run. + ContinueOnError bool `xorm:"NOT NULL DEFAULT FALSE"` + Started timeutil.TimeStamp Stopped timeutil.TimeStamp Created timeutil.TimeStamp `xorm:"created"` @@ -357,6 +361,14 @@ func CollectAllDescendantJobs(parent *ActionRunJob, allJobs []*ActionRunJob) []* return out } +// hasWaitingJobsToPick reports whether any waiting, unclaimed, non-reusable job +// remains in the repo, i.e. work that an idle runner could still pick up. +func hasWaitingJobsToPick(ctx context.Context, repoID int64) (bool, error) { + return db.GetEngine(ctx). + Where("repo_id = ? AND task_id = ? AND status = ? AND is_reusable_caller = ?", repoID, 0, StatusWaiting, false). + Exist(&ActionRunJob{}) +} + func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, cols ...string) (int64, error) { e := db.GetEngine(ctx) @@ -381,14 +393,6 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col return affected, nil } - // Reusable workflow caller jobs are never picked up by runners, so they don't need a task-version bump. - if statusUpdated && job.Status.IsWaiting() && !job.IsReusableCaller { - // if the status of job changes to waiting again, increase tasks version. - if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil { - return 0, err - } - } - if job.RunID == 0 { var err error if job, err = GetRunJobByRepoAndID(ctx, job.RepoID, job.ID); err != nil { @@ -396,6 +400,37 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col } } + // Reusable workflow caller jobs are never picked up by runners, so they don't need a task-version bump. + if statusUpdated && !job.IsReusableCaller { + switch { + case job.Status.IsWaiting(): + // A job returning to the waiting queue is work a runner can pick up, so bump the + // version to wake idle runners whose tasksVersion already equals latestVersion. + if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil { + return 0, err + } + case job.Status.IsDone(): + // When a job finishes, bump the version so that idle runners — whose + // tasksVersion already equals the current latestVersion — learn that + // remaining waiting jobs are still available and attempt PickTask again. + // Without this bump, runners that completed their tasks would see + // tasksVersion==latestVersion and skip PickTask, leaving the other jobs + // permanently unassigned until the version changes for another reason. + // Only bump when waiting work actually remains for this repo, otherwise + // every job completion would needlessly bump the global version and wake + // every idle runner instance-wide for nothing. + hasWaiting, err := hasWaitingJobsToPick(ctx, job.RepoID) + if err != nil { + return 0, err + } + if hasWaiting { + if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil { + return 0, err + } + } + } + } + if statusUpdated && job.ParentJobID > 0 { // Reusable workflow caller's children cascade their status changes upward to the parent caller. parent, err := GetRunJobByRunAndID(ctx, job.RunID, job.ParentJobID) @@ -500,9 +535,12 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status { allSkipped := len(jobs) != 0 var hasFailure, hasCancelled, hasCancelling, hasWaiting, hasRunning, hasBlocked bool for _, job := range jobs { - allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped) + // A failed job with continue-on-error:true does not fail the workflow run. + // It counts as a "continued failure" and is treated like success for aggregation. + isContinuedFailure := job.ContinueOnError && job.Status == StatusFailure + allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped || isContinuedFailure) allSkipped = allSkipped && job.Status == StatusSkipped - hasFailure = hasFailure || job.Status == StatusFailure + hasFailure = hasFailure || (job.Status == StatusFailure && !job.ContinueOnError) hasCancelled = hasCancelled || job.Status == StatusCancelled hasCancelling = hasCancelling || job.Status == StatusCancelling hasWaiting = hasWaiting || job.Status == StatusWaiting diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go index f08f7c84a6..9ef9865bdf 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -99,6 +99,10 @@ type FindRunJobOptions struct { UpdatedBefore timeutil.TimeStamp ConcurrencyGroup string OrderBy db.SearchOrderBy + // AccessibleRepoIDsSubQuery, when non-nil, restricts results to the repo IDs selected by the + // subquery (the caller's accessible repos). A nil value means no restriction. Using a subquery + // instead of a materialized ID slice avoids exceeding DB parameter limits for large owners. + AccessibleRepoIDsSubQuery *builder.Builder } var JobOrderByMap = map[string]map[string]db.SearchOrderBy{ @@ -132,6 +136,9 @@ func (opts FindRunJobOptions) ToConds() builder.Cond { } cond = cond.And(builder.Eq{"`action_run_job`.concurrency_group": opts.ConcurrencyGroup}) } + if opts.AccessibleRepoIDsSubQuery != nil { + cond = cond.And(builder.In("`action_run_job`.repo_id", opts.AccessibleRepoIDsSubQuery)) + } return cond } diff --git a/models/actions/run_list.go b/models/actions/run_list.go index 88f3d3dd82..7bfe3314d6 100644 --- a/models/actions/run_list.go +++ b/models/actions/run_list.go @@ -10,6 +10,7 @@ import ( repo_model "gitea.dev/models/repo" user_model "gitea.dev/models/user" "gitea.dev/modules/container" + "gitea.dev/modules/optional" "gitea.dev/modules/translation" webhook_module "gitea.dev/modules/webhook" @@ -61,12 +62,18 @@ type FindRunOptions struct { RepoID int64 OwnerID int64 WorkflowID string - Ref string // the commit/tag/… that caused this workflow + WorkflowRepoID int64 // source-aware filter: the repo a run's workflow content came from (0 = any) + IsScopedRun optional.Option[bool] // is the run from a scoped workflow + Ref string // the commit/tag/… that caused this workflow TriggerUserID int64 TriggerEvent webhook_module.HookEventType Status []Status ConcurrencyGroup string CommitSHA string + // AccessibleRepoIDsSubQuery, when non-nil, restricts results to the repo IDs selected by the + // subquery (the caller's accessible repos). A nil value means no restriction. Using a subquery + // instead of a materialized ID slice avoids exceeding DB parameter limits for large owners. + AccessibleRepoIDsSubQuery *builder.Builder } func (opts FindRunOptions) ToConds() builder.Cond { @@ -77,6 +84,12 @@ func (opts FindRunOptions) ToConds() builder.Cond { if opts.WorkflowID != "" { cond = cond.And(builder.Eq{"`action_run`.workflow_id": opts.WorkflowID}) } + if opts.WorkflowRepoID > 0 { + cond = cond.And(builder.Eq{"`action_run`.workflow_repo_id": opts.WorkflowRepoID}) + } + if opts.IsScopedRun.Has() { + cond = cond.And(builder.Eq{"`action_run`.is_scoped_run": opts.IsScopedRun.Value()}) + } if opts.TriggerUserID > 0 { cond = cond.And(builder.Eq{"`action_run`.trigger_user_id": opts.TriggerUserID}) } @@ -92,6 +105,9 @@ func (opts FindRunOptions) ToConds() builder.Cond { if opts.CommitSHA != "" { cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA}) } + if opts.AccessibleRepoIDsSubQuery != nil { + cond = cond.And(builder.In("`action_run`.repo_id", opts.AccessibleRepoIDsSubQuery)) + } return cond } @@ -106,6 +122,15 @@ func (opts FindRunOptions) ToJoins() []db.JoinFunc { } func (opts FindRunOptions) ToOrders() string { + // When scoped to a repo, sort by `index`: it reuses the unique + // `repo_index` (repo_id, index) index, so the query seeks repo_id and + // walks index descending instead of filesorting all matching rows. + // Within a repo `index` is co-monotonic with `id`, so the order is the same. + if opts.RepoID > 0 { + return "`action_run`.`index` DESC" + } + // `index` is scoped per repo, so it is meaningless across repos. With no + // RepoID, sort by the global, PK-indexed `id` for a deterministic order. return "`action_run`.`id` DESC" } @@ -117,8 +142,8 @@ type StatusInfo struct { // GetStatusInfoList returns a slice of StatusInfo func GetStatusInfoList(ctx context.Context, lang translation.Locale) []StatusInfo { - // same as those in aggregateJobStatus - allStatus := []Status{StatusSuccess, StatusFailure, StatusWaiting, StatusRunning, StatusCancelling} + // same as those in aggregateJobStatus (StatusUnknown excluded; it's the "shouldn't happen" fallback) + allStatus := []Status{StatusSuccess, StatusFailure, StatusCancelled, StatusSkipped, StatusWaiting, StatusRunning, StatusBlocked, StatusCancelling} statusInfoList := make([]StatusInfo, 0, len(allStatus)) for _, s := range allStatus { statusInfoList = append(statusInfoList, StatusInfo{ @@ -147,9 +172,20 @@ func GetRunBranches(ctx context.Context, repoID int64) ([]string, error) { // GetRunWorkflowIDs returns all distinct WorkflowIDs that have at least // one ActionRun in the given repo. func GetRunWorkflowIDs(ctx context.Context, repoID int64) ([]string, error) { + return getRunWorkflowIDs(ctx, repoID, builder.NewCond()) +} + +// GetRepoRunWorkflowIDs returns all distinct WorkflowIDs that have at least +// one repo-level ActionRun in the given repo. +func GetRepoRunWorkflowIDs(ctx context.Context, repoID int64) ([]string, error) { + return getRunWorkflowIDs(ctx, repoID, builder.Eq{"is_scoped_run": false}) +} + +func getRunWorkflowIDs(ctx context.Context, repoID int64, extraCond builder.Cond) ([]string, error) { ids := make([]string, 0, 10) + cond := builder.Eq{"repo_id": repoID} return ids, db.GetEngine(ctx).Table("action_run"). - Where(builder.Eq{"repo_id": repoID}). + Where(cond.And(extraCond)). Distinct("workflow_id"). Cols("workflow_id"). Asc("workflow_id"). diff --git a/models/actions/run_list_test.go b/models/actions/run_list_test.go index 74f630bb55..ce8d23182f 100644 --- a/models/actions/run_list_test.go +++ b/models/actions/run_list_test.go @@ -6,10 +6,13 @@ package actions import ( "testing" + "gitea.dev/models/db" "gitea.dev/models/unittest" + "gitea.dev/modules/optional" "gitea.dev/modules/translation" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetRunWorkflowIDs(t *testing.T) { @@ -24,14 +27,139 @@ func TestGetRunWorkflowIDs(t *testing.T) { assert.Empty(t, ids) } +func TestGetRepoRunWorkflowIDs(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + const ( + repoID = int64(4) + repoWorkflowID = "repo-orphan.yaml" + scopedWorkflowID = "scoped-only.yaml" + sharedWorkflowID = "shared-name.yaml" + scopedWorkflowRepo = int64(111) + ) + for _, spec := range []struct { + id int64 + workflowID string + workflowRepoID int64 + isScopedRun bool + }{ + {99811, repoWorkflowID, repoID, false}, + {99812, scopedWorkflowID, scopedWorkflowRepo, true}, + {99813, sharedWorkflowID, repoID, false}, + {99814, sharedWorkflowID, scopedWorkflowRepo, true}, + } { + require.NoError(t, db.Insert(t.Context(), &ActionRun{ + ID: spec.id, + Index: spec.id, + RepoID: repoID, + OwnerID: 1, + TriggerUserID: 1, + WorkflowID: spec.workflowID, + WorkflowRepoID: spec.workflowRepoID, + IsScopedRun: spec.isScopedRun, + })) + } + + ids, err := GetRepoRunWorkflowIDs(t.Context(), repoID) + require.NoError(t, err) + assert.Contains(t, ids, repoWorkflowID) + assert.Contains(t, ids, sharedWorkflowID) + assert.NotContains(t, ids, scopedWorkflowID) +} + func TestGetStatusInfoList(t *testing.T) { statusInfoList := GetStatusInfoList(t.Context(), translation.MockLocale{}) assert.Equal(t, []StatusInfo{ {Status: int(StatusSuccess), StatusName: StatusSuccess.String(), DisplayedStatus: "actions.status.success"}, {Status: int(StatusFailure), StatusName: StatusFailure.String(), DisplayedStatus: "actions.status.failure"}, + {Status: int(StatusCancelled), StatusName: StatusCancelled.String(), DisplayedStatus: "actions.status.cancelled"}, + {Status: int(StatusSkipped), StatusName: StatusSkipped.String(), DisplayedStatus: "actions.status.skipped"}, {Status: int(StatusWaiting), StatusName: StatusWaiting.String(), DisplayedStatus: "actions.status.waiting"}, {Status: int(StatusRunning), StatusName: StatusRunning.String(), DisplayedStatus: "actions.status.running"}, + {Status: int(StatusBlocked), StatusName: StatusBlocked.String(), DisplayedStatus: "actions.status.blocked"}, {Status: int(StatusCancelling), StatusName: StatusCancelling.String(), DisplayedStatus: "actions.status.cancelling"}, }, statusInfoList) } + +// TestFindRunOptions_WorkflowRepoID: two runs share the bare WorkflowID but come from different content-source repos; +// the source-aware WorkflowRepoID filter must separate them. +func TestFindRunOptions_WorkflowRepoID(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + const ( + repoID = int64(4) + sourceA = int64(111) + sourceB = int64(222) + workflowID = "u3-shared.yaml" + ) + for _, spec := range []struct{ id, workflowRepoID int64 }{ + {99801, sourceA}, + {99802, sourceB}, + } { + require.NoError(t, db.Insert(t.Context(), &ActionRun{ + ID: spec.id, + Index: spec.id, + RepoID: repoID, + OwnerID: 1, + TriggerUserID: 1, + WorkflowID: workflowID, + WorkflowRepoID: spec.workflowRepoID, + IsScopedRun: true, + })) + } + + // no source filter -> both + all, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID}) + require.NoError(t, err) + assert.Len(t, all, 2) + + // filter by source A -> only the run whose content came from A + onlyA, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID, WorkflowRepoID: sourceA}) + require.NoError(t, err) + require.Len(t, onlyA, 1) + assert.EqualValues(t, 99801, onlyA[0].ID) + + // filter by source B -> only the run whose content came from B + onlyB, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID, WorkflowRepoID: sourceB}) + require.NoError(t, err) + require.Len(t, onlyB, 1) + assert.EqualValues(t, 99802, onlyB[0].ID) +} + +func TestFindRunOptions_IsScopedRun(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + const ( + repoID = int64(4) + workflowID = "scoped-flag.yaml" + ) + for _, spec := range []struct { + id int64 + scoped bool + }{ + {99821, false}, + {99822, true}, + } { + require.NoError(t, db.Insert(t.Context(), &ActionRun{ + ID: spec.id, + Index: spec.id, + RepoID: repoID, + OwnerID: 1, + TriggerUserID: 1, + WorkflowID: workflowID, + WorkflowRepoID: repoID, + IsScopedRun: spec.scoped, + })) + } + + repoLevel, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID, IsScopedRun: optional.Some(false)}) + require.NoError(t, err) + require.Len(t, repoLevel, 1) + assert.EqualValues(t, 99821, repoLevel[0].ID) + + scoped, err := db.Find[ActionRun](t.Context(), FindRunOptions{RepoID: repoID, WorkflowID: workflowID, IsScopedRun: optional.Some(true)}) + require.NoError(t, err) + require.Len(t, scoped, 1) + assert.EqualValues(t, 99822, scoped[0].ID) +} diff --git a/models/actions/run_test.go b/models/actions/run_test.go index 356ed28a74..08330cd999 100644 --- a/models/actions/run_test.go +++ b/models/actions/run_test.go @@ -13,6 +13,7 @@ import ( "gitea.dev/modules/timeutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestUpdateRepoRunsNumbers(t *testing.T) { @@ -44,3 +45,57 @@ func TestActionRun_Duration_NonNegative(t *testing.T) { } assert.Equal(t, time.Duration(0), run.Duration()) } + +func TestActionRun_WorkflowLink(t *testing.T) { + repo := &repo_model.Repository{OwnerName: "org", Name: "consumer"} + + // a repo-level run links by file name only + repoLevel := &ActionRun{Repo: repo, WorkflowID: "ci.yaml", WorkflowRepoID: repo.ID} + assert.Equal(t, repo.Link()+"/actions/?workflow=ci.yaml", repoLevel.WorkflowLink()) + + // a scoped run carries its source repo id back, so the list stays filtered to that source + scoped := &ActionRun{Repo: repo, WorkflowID: "ci.yaml", WorkflowRepoID: 42, IsScopedRun: true} + assert.Equal(t, repo.Link()+"/actions/?workflow=ci.yaml&scoped_workflow_source_repo_id=42", scoped.WorkflowLink()) +} + +func TestGetWorkflowLatestRun_RepoLevelOnly(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + const ( + repoID = int64(4) + workflowID = "badge-source-aware.yaml" + ref = "refs/heads/main" + ) + require.NoError(t, db.Insert(t.Context(), &ActionRun{ + ID: 99811, + Index: 99811, + RepoID: repoID, + OwnerID: 1, + TriggerUserID: 1, + WorkflowID: workflowID, + Ref: ref, + Event: "push", + Status: StatusSuccess, + WorkflowRepoID: repoID, + WorkflowCommitSHA: "repo-level-sha", + })) + require.NoError(t, db.Insert(t.Context(), &ActionRun{ + ID: 99812, + Index: 99812, + RepoID: repoID, + OwnerID: 1, + TriggerUserID: 1, + WorkflowID: workflowID, + Ref: ref, + Event: "push", + Status: StatusFailure, + WorkflowRepoID: 111, + WorkflowCommitSHA: "scoped-sha", + IsScopedRun: true, + })) + + run, err := GetWorkflowLatestRun(t.Context(), repoID, workflowID, ref, "push") + require.NoError(t, err) + assert.EqualValues(t, 99811, run.ID) + assert.False(t, run.IsScopedRun) +} diff --git a/models/actions/runner.go b/models/actions/runner.go index ba2b38ee7e..bc4367b52e 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -75,8 +75,28 @@ type ActionRunner struct { const ( RunnerOfflineTime = time.Minute RunnerIdleTime = 10 * time.Second + // RunnerHeartbeatInterval is how often last_online is persisted on poll. + // Must stay well below RunnerOfflineTime so runners don't flap to offline. + RunnerHeartbeatInterval = 30 * time.Second + // RunnerActiveInterval is how often last_active is persisted while a runner + // streams task updates and logs. Must stay well below RunnerIdleTime so a + // busy runner keeps showing ACTIVE without a DB write on every RPC. + RunnerActiveInterval = 5 * time.Second ) +// ShouldPersistLastOnline reports whether last_online is stale enough to be +// worth writing back. Avoids a DB write on every runner poll. +func ShouldPersistLastOnline(last timeutil.TimeStamp, now time.Time) bool { + return now.Sub(last.AsTime()) >= RunnerHeartbeatInterval +} + +// ShouldPersistLastActive reports whether last_active is stale enough to be +// worth writing back. Avoids a DB write on every UpdateTask/UpdateLog RPC while +// a runner is actively streaming logs. +func ShouldPersistLastActive(last timeutil.TimeStamp, now time.Time) bool { + return now.Sub(last.AsTime()) >= RunnerActiveInterval +} + // BelongsToOwnerName before calling, should guarantee that all attributes are loaded func (r *ActionRunner) BelongsToOwnerName() string { if r.RepoID != 0 { @@ -251,21 +271,24 @@ func (opts FindRunnerOptions) ToConds() builder.Cond { } func (opts FindRunnerOptions) ToOrders() string { + // A unique tiebreaker (id) is appended so that runners sharing the same + // last_online or name keep a deterministic order across paginated queries, + // otherwise the same runner may appear on more than one page. switch opts.Sort { case "online": - return "last_online DESC" + return "last_online DESC, id ASC" case "offline": - return "last_online ASC" + return "last_online ASC, id ASC" case "alphabetically": - return "name ASC" + return "name ASC, id ASC" case "reversealphabetically": - return "name DESC" + return "name DESC, id ASC" case "newest": return "id DESC" case "oldest": return "id ASC" } - return "last_online DESC" + return "last_online DESC, id ASC" } // GetRunnerByUUID returns a runner via uuid diff --git a/models/actions/runner_test.go b/models/actions/runner_test.go new file mode 100644 index 0000000000..47b8a69307 --- /dev/null +++ b/models/actions/runner_test.go @@ -0,0 +1,149 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "fmt" + "testing" + "time" + + "gitea.dev/models/db" + "gitea.dev/models/unittest" + "gitea.dev/modules/timeutil" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestShouldPersistLastOnline(t *testing.T) { + now := time.Now() + tests := []struct { + name string + last timeutil.TimeStamp + want bool + }{ + { + name: "fresh, skip write", + last: timeutil.TimeStamp(now.Add(-5 * time.Second).Unix()), + want: false, + }, + { + name: "exactly at interval, write", + last: timeutil.TimeStamp(now.Add(-RunnerHeartbeatInterval).Unix()), + want: true, + }, + { + name: "stale, write", + last: timeutil.TimeStamp(now.Add(-2 * RunnerHeartbeatInterval).Unix()), + want: true, + }, + { + name: "zero (never seen), write", + last: 0, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, ShouldPersistLastOnline(tt.last, now)) + }) + } +} + +func TestShouldPersistLastActive(t *testing.T) { + now := time.Now() + tests := []struct { + name string + last timeutil.TimeStamp + want bool + }{ + { + name: "fresh, skip write", + last: timeutil.TimeStamp(now.Add(-1 * time.Second).Unix()), + want: false, + }, + { + name: "exactly at interval, write", + last: timeutil.TimeStamp(now.Add(-RunnerActiveInterval).Unix()), + want: true, + }, + { + name: "stale, write", + last: timeutil.TimeStamp(now.Add(-2 * RunnerActiveInterval).Unix()), + want: true, + }, + { + name: "zero (never seen), write", + last: 0, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, ShouldPersistLastActive(tt.last, now)) + }) + } +} + +func TestFindRunnerOptions_ToOrders_StableTiebreaker(t *testing.T) { + // Sorts on a non-unique column must end with the unique id tiebreaker so + // pagination is deterministic; without it, runners sharing the same + // last_online or name can appear on more than one page. Sorts already on + // the unique id need no tiebreaker. + expected := map[string]string{ + "": "last_online DESC, id ASC", + "online": "last_online DESC, id ASC", + "offline": "last_online ASC, id ASC", + "alphabetically": "name ASC, id ASC", + "reversealphabetically": "name DESC, id ASC", + "newest": "id DESC", + "oldest": "id ASC", + } + for sort, want := range expected { + assert.Equal(t, want, FindRunnerOptions{Sort: sort}.ToOrders(), "sort %q", sort) + } +} + +func TestFindRunners_PaginationNoDuplicates(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + ctx := t.Context() + + // Create several runners that all share the same last_online value so the + // primary sort key (last_online) is tied for all of them. + const ownerID = 1000 + const count = 6 + for i := range count { + runner := &ActionRunner{ + Name: "paginated-runner", + UUID: fmt.Sprintf("PAGINATE-TEST-0000-0000-00000000000%d", i), + TokenHash: fmt.Sprintf("paginate-test-token-hash-%d", i), + OwnerID: ownerID, + RepoID: 0, + LastOnline: 42, + } + require.NoError(t, db.Insert(ctx, runner)) + } + + // Page through the runners and ensure every id is returned exactly once. + seen := make(map[int64]int) + const pageSize = 2 + for page := 1; ; page++ { + runners, err := db.Find[ActionRunner](ctx, FindRunnerOptions{ + ListOptions: db.ListOptions{Page: page, PageSize: pageSize}, + OwnerID: ownerID, + }) + require.NoError(t, err) + if len(runners) == 0 { + break + } + for _, r := range runners { + seen[r.ID]++ + } + } + + assert.Len(t, seen, count, "each runner should be returned exactly once across all pages") + for id, n := range seen { + assert.Equal(t, 1, n, "runner %d appeared on %d pages", id, n) + } +} diff --git a/models/actions/scoped_workflow.go b/models/actions/scoped_workflow.go new file mode 100644 index 0000000000..310e1a041d --- /dev/null +++ b/models/actions/scoped_workflow.go @@ -0,0 +1,179 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "fmt" + + "gitea.dev/models/db" + repo_model "gitea.dev/models/repo" + "gitea.dev/modules/timeutil" + "gitea.dev/modules/util" + + "xorm.io/builder" +) + +// ActionScopedWorkflowSource registers a repository as a source of scoped workflows, either for an owner (user/org) or for the whole instance. +type ActionScopedWorkflowSource struct { + ID int64 `xorm:"pk autoincr"` + + // OwnerID is the scope the source applies to: a user/org ID (applies to that owner's repos), or 0 for instance-level (applies to every repo). + OwnerID int64 `xorm:"UNIQUE(owner_repo) NOT NULL DEFAULT 0"` + // SourceRepoID is the source repository providing the workflow files; always non-zero. + SourceRepoID int64 `xorm:"INDEX UNIQUE(owner_repo) NOT NULL DEFAULT 0"` + + // WorkflowConfigs maps a workflow ID (entry name) to its merge-gate config. + WorkflowConfigs map[string]*ScopedWorkflowConfig `xorm:"JSON TEXT 'workflow_configs'"` + + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` +} + +// ScopedWorkflowConfig is one scoped workflow's config within a source registration. +type ScopedWorkflowConfig struct { + Required bool `json:"required"` + Patterns []string `json:"patterns"` // the status-check patterns that must be present and pass, only effective when Required is true +} + +func init() { + db.RegisterModel(new(ActionScopedWorkflowSource)) +} + +// IsWorkflowRequired reports whether the given workflow ID (entry name) is marked required in this source. +func (s *ActionScopedWorkflowSource) IsWorkflowRequired(workflowID string) bool { + c, ok := s.WorkflowConfigs[workflowID] + return ok && c.Required +} + +type FindScopedWorkflowSourceOpts struct { + db.ListOptions + OwnerIDs []int64 + SourceRepoID int64 +} + +func (opts FindScopedWorkflowSourceOpts) ToConds() builder.Cond { + cond := builder.NewCond() + if len(opts.OwnerIDs) > 0 { + cond = cond.And(builder.In("owner_id", opts.OwnerIDs)) + } + if opts.SourceRepoID != 0 { + cond = cond.And(builder.Eq{"source_repo_id": opts.SourceRepoID}) + } + return cond +} + +// GetEffectiveScopedWorkflowSources returns the scoped-workflow sources effective for a repo owned by repoOwnerID: +// the owner's own sources plus instance-level (owner_id=0) sources. +func GetEffectiveScopedWorkflowSources(ctx context.Context, repoOwnerID int64) ([]*ActionScopedWorkflowSource, error) { + owners := []int64{0} + if repoOwnerID != 0 { + owners = append(owners, repoOwnerID) + } + return db.Find[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: owners}) +} + +// IsScopedWorkflowSourceEffective reports whether sourceRepoID is a scoped-workflow source effective for a repo owned by repoOwnerID. +func IsScopedWorkflowSourceEffective(ctx context.Context, repoOwnerID, sourceRepoID int64) (bool, error) { + owners := []int64{0} + if repoOwnerID != 0 { + owners = append(owners, repoOwnerID) + } + return db.Exist[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: owners, SourceRepoID: sourceRepoID}.ToConds()) +} + +// IsWorkflowRequiredInSources reports whether workflowID from sourceRepoID is required by any of the given sources. +func IsWorkflowRequiredInSources(sources []*ActionScopedWorkflowSource, sourceRepoID int64, workflowID string) bool { + for _, s := range sources { + if s.SourceRepoID == sourceRepoID && s.IsWorkflowRequired(workflowID) { + return true + } + } + return false +} + +// ScopedStatusContextPrefix returns the source-repo prefix that makes a scoped run's commit-status context distinct from same-named workflows. +func ScopedStatusContextPrefix(ctx context.Context, sourceRepoID int64) string { + if sourceRepo, err := repo_model.GetRepositoryByID(ctx, sourceRepoID); err == nil { + return sourceRepo.FullName() + } + return fmt.Sprintf("scoped:%d", sourceRepoID) +} + +// IsScopedWorkflowRequired reports whether workflowID from sourceRepoID is required for a repo owned by consumerOwnerID. +func IsScopedWorkflowRequired(ctx context.Context, consumerOwnerID, sourceRepoID int64, workflowID string) (bool, error) { + sources, err := GetEffectiveScopedWorkflowSources(ctx, consumerOwnerID) + if err != nil { + return false, err + } + return IsWorkflowRequiredInSources(sources, sourceRepoID, workflowID), nil +} + +// IsScopedWorkflowOptedOutloads the consumer's effective sources then calls ScopedWorkflowOptedOut +func IsScopedWorkflowOptedOut(ctx context.Context, cfg *repo_model.ActionsConfig, consumerOwnerID, sourceRepoID int64, workflowID string) (bool, error) { + if !cfg.IsScopedWorkflowDisabled(sourceRepoID, workflowID) { + return false, nil + } + sources, err := GetEffectiveScopedWorkflowSources(ctx, consumerOwnerID) + if err != nil { + return false, err + } + return ScopedWorkflowOptedOut(cfg, sources, sourceRepoID, workflowID), nil +} + +// ScopedWorkflowOptedOut reports whether a consumer's opt-out of (sourceRepoID, workflowID) is in effect. +func ScopedWorkflowOptedOut(cfg *repo_model.ActionsConfig, sources []*ActionScopedWorkflowSource, sourceRepoID int64, workflowID string) bool { + return !IsWorkflowRequiredInSources(sources, sourceRepoID, workflowID) && cfg.IsScopedWorkflowDisabled(sourceRepoID, workflowID) +} + +// GetScopedWorkflowSourcesByOwner returns the sources an owner (user/org, or 0 for instance) registered. +func GetScopedWorkflowSourcesByOwner(ctx context.Context, ownerID int64) ([]*ActionScopedWorkflowSource, error) { + return db.Find[ActionScopedWorkflowSource](ctx, FindScopedWorkflowSourceOpts{OwnerIDs: []int64{ownerID}}) +} + +// GetScopedWorkflowSource returns the (owner, repo) source registration or a NotExist error. +func GetScopedWorkflowSource(ctx context.Context, ownerID, repoID int64) (*ActionScopedWorkflowSource, error) { + src := &ActionScopedWorkflowSource{} + has, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Get(src) + if err != nil { + return nil, err + } + if !has { + return nil, util.NewNotExistErrorf("scoped workflow source (owner %d, repo %d) does not exist", ownerID, repoID) + } + return src, nil +} + +// AddScopedWorkflowSource registers repoID as a source for ownerID (no-op if already registered). +func AddScopedWorkflowSource(ctx context.Context, ownerID, repoID int64) error { + exists, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Exist(new(ActionScopedWorkflowSource)) + if err != nil { + return err + } + if exists { + return nil + } + if err := db.Insert(ctx, &ActionScopedWorkflowSource{OwnerID: ownerID, SourceRepoID: repoID}); err != nil { + // Re-check and treat an already-present row as the intended no-op. + if exists, existErr := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Exist(new(ActionScopedWorkflowSource)); existErr == nil && exists { + return nil + } + return err + } + return nil +} + +// SetScopedWorkflowSourceConfigs replaces the per-workflow merge-gate configs (workflow ID -> config). +func SetScopedWorkflowSourceConfigs(ctx context.Context, ownerID, repoID int64, configs map[string]*ScopedWorkflowConfig) error { + _, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID). + Cols("workflow_configs"). + Update(&ActionScopedWorkflowSource{WorkflowConfigs: configs}) + return err +} + +// RemoveScopedWorkflowSource removes the (owner, repo) source registration. +func RemoveScopedWorkflowSource(ctx context.Context, ownerID, repoID int64) error { + _, err := db.GetEngine(ctx).Where("owner_id = ? AND source_repo_id = ?", ownerID, repoID).Delete(new(ActionScopedWorkflowSource)) + return err +} diff --git a/models/actions/scoped_workflow_test.go b/models/actions/scoped_workflow_test.go new file mode 100644 index 0000000000..f3b90f6cd1 --- /dev/null +++ b/models/actions/scoped_workflow_test.go @@ -0,0 +1,139 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + "gitea.dev/models/db" + "gitea.dev/models/unittest" + "gitea.dev/modules/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestScopedWorkflowSource_IsWorkflowRequired(t *testing.T) { + src := &ActionScopedWorkflowSource{WorkflowConfigs: map[string]*ScopedWorkflowConfig{ + "a.yml": {Required: true, Patterns: []string{"p"}}, + "b.yml": {Required: true, Patterns: []string{"p"}}, + "c.yml": {Required: false, Patterns: []string{"p"}}, // patterns kept as history, not required + }} + assert.True(t, src.IsWorkflowRequired("a.yml")) + assert.True(t, src.IsWorkflowRequired("b.yml")) + assert.False(t, src.IsWorkflowRequired("c.yml"), "config kept as history but not required") + assert.False(t, src.IsWorkflowRequired("d.yml")) + + empty := &ActionScopedWorkflowSource{} + assert.False(t, empty.IsWorkflowRequired("a.yml")) +} + +func TestIsWorkflowRequiredInSources(t *testing.T) { + // repo 100 registered twice (org optional + instance required). + sources := []*ActionScopedWorkflowSource{ + {OwnerID: 2, SourceRepoID: 100, WorkflowConfigs: nil}, + {OwnerID: 0, SourceRepoID: 100, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"a.yml": {Required: true, Patterns: []string{"p"}}}}, + {OwnerID: 0, SourceRepoID: 200, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"b.yml": {Required: true, Patterns: []string{"p"}}}}, + } + + assert.True(t, IsWorkflowRequiredInSources(sources, 100, "a.yml"), "required at instance level wins over org optional") + assert.False(t, IsWorkflowRequiredInSources(sources, 100, "z.yml")) + assert.False(t, IsWorkflowRequiredInSources(sources, 200, "a.yml"), "a.yml is required for repo 100, not repo 200") + assert.True(t, IsWorkflowRequiredInSources(sources, 200, "b.yml")) + assert.False(t, IsWorkflowRequiredInSources(sources, 999, "a.yml"), "unknown source repo") +} + +func TestGetEffectiveScopedWorkflowSources(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + ctx := t.Context() + + rows := []*ActionScopedWorkflowSource{ + {OwnerID: 2, SourceRepoID: 100, WorkflowConfigs: nil}, // org 2 registers repo 100 (optional) + {OwnerID: 0, SourceRepoID: 100, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"a.yml": {Required: true, Patterns: []string{"p"}}}}, // instance also registers repo 100 (required) + {OwnerID: 0, SourceRepoID: 200, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"b.yml": {Required: true, Patterns: []string{"p"}}}}, // instance source 200 + {OwnerID: 3, SourceRepoID: 300, WorkflowConfigs: map[string]*ScopedWorkflowConfig{"c.yml": {Required: true, Patterns: []string{"p"}}}}, // a different owner's source + } + for _, r := range rows { + require.NoError(t, db.Insert(ctx, r)) + } + + // owner 2 sees its own sources plus instance-level ones, but not owner 3's. + owner2, err := GetEffectiveScopedWorkflowSources(ctx, 2) + require.NoError(t, err) + assert.Len(t, owner2, 3) + + required, err := IsScopedWorkflowRequired(ctx, 2, 100, "a.yml") + require.NoError(t, err) + assert.True(t, required, "instance marks a.yml required → required for owner 2 even though org left it optional") + + required, err = IsScopedWorkflowRequired(ctx, 2, 100, "x.yml") + require.NoError(t, err) + assert.False(t, required) + + required, err = IsScopedWorkflowRequired(ctx, 2, 200, "b.yml") + require.NoError(t, err) + assert.True(t, required) + + // owner 3's source must not be effective for owner 2. + required, err = IsScopedWorkflowRequired(ctx, 2, 300, "c.yml") + require.NoError(t, err) + assert.False(t, required) + + // IsScopedWorkflowSourceEffective: owner-level and instance-level sources are effective; another owner's is not. + effective, err := IsScopedWorkflowSourceEffective(ctx, 2, 100) + require.NoError(t, err) + assert.True(t, effective, "owner 2's own source") + + effective, err = IsScopedWorkflowSourceEffective(ctx, 2, 200) + require.NoError(t, err) + assert.True(t, effective, "instance-level source is effective for any owner") + + effective, err = IsScopedWorkflowSourceEffective(ctx, 2, 300) + require.NoError(t, err) + assert.False(t, effective, "owner 3's source is not effective for owner 2") + + effective, err = IsScopedWorkflowSourceEffective(ctx, 2, 999) + require.NoError(t, err) + assert.False(t, effective, "unknown source repo") + + effective, err = IsScopedWorkflowSourceEffective(ctx, 3, 300) + require.NoError(t, err) + assert.True(t, effective, "owner 3's own source is effective for owner 3") +} + +func TestScopedWorkflowSourceCRUD(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + ctx := t.Context() + + // add is idempotent + require.NoError(t, AddScopedWorkflowSource(ctx, 5, 10)) + require.NoError(t, AddScopedWorkflowSource(ctx, 5, 10)) + sources, err := GetScopedWorkflowSourcesByOwner(ctx, 5) + require.NoError(t, err) + assert.Len(t, sources, 1) + + // set the per-workflow configs (entry name -> {required, patterns}); a.yml required, b.yml kept as history (not required) + configs := map[string]*ScopedWorkflowConfig{ + "a.yml": {Required: true, Patterns: []string{"src: a.yml / *"}}, + "b.yml": {Required: false, Patterns: []string{"src: b.yml / build (push)"}}, + } + require.NoError(t, SetScopedWorkflowSourceConfigs(ctx, 5, 10, configs)) + src, err := GetScopedWorkflowSource(ctx, 5, 10) + require.NoError(t, err) + assert.Equal(t, configs, src.WorkflowConfigs) + + // clearing the configs works + require.NoError(t, SetScopedWorkflowSourceConfigs(ctx, 5, 10, nil)) + src, err = GetScopedWorkflowSource(ctx, 5, 10) + require.NoError(t, err) + assert.Empty(t, src.WorkflowConfigs) + + // remove + require.NoError(t, RemoveScopedWorkflowSource(ctx, 5, 10)) + _, err = GetScopedWorkflowSource(ctx, 5, 10) + assert.ErrorIs(t, err, util.ErrNotExist) + sources, err = GetScopedWorkflowSourcesByOwner(ctx, 5) + require.NoError(t, err) + assert.Empty(t, sources) +} diff --git a/models/actions/status_test.go b/models/actions/status_test.go index f1551b2892..eed45af9b0 100644 --- a/models/actions/status_test.go +++ b/models/actions/status_test.go @@ -48,3 +48,57 @@ func TestStatusFromResult(t *testing.T) { assert.Equal(t, tt.want, StatusFromResult(tt.result), "result=%s", tt.result) } } + +func newJob(status Status, continueOnError bool) *ActionRunJob { + return &ActionRunJob{Status: status, ContinueOnError: continueOnError} +} + +func TestAggregateJobStatusContinueOnError(t *testing.T) { + cases := []struct { + name string + jobs []*ActionRunJob + want Status + }{ + { + name: "all success", + jobs: []*ActionRunJob{newJob(StatusSuccess, false), newJob(StatusSuccess, false)}, + want: StatusSuccess, + }, + { + name: "one failure without continue-on-error", + jobs: []*ActionRunJob{newJob(StatusSuccess, false), newJob(StatusFailure, false)}, + want: StatusFailure, + }, + { + name: "one failure with continue-on-error", + jobs: []*ActionRunJob{newJob(StatusSuccess, false), newJob(StatusFailure, true)}, + want: StatusSuccess, + }, + { + name: "only continued-failure", + jobs: []*ActionRunJob{newJob(StatusFailure, true)}, + want: StatusSuccess, + }, + { + name: "continued-failure plus real failure", + jobs: []*ActionRunJob{newJob(StatusFailure, true), newJob(StatusFailure, false)}, + want: StatusFailure, + }, + { + name: "all skipped", + jobs: []*ActionRunJob{newJob(StatusSkipped, false), newJob(StatusSkipped, false)}, + want: StatusSkipped, + }, + { + name: "continued-failure plus skipped counts as success", + jobs: []*ActionRunJob{newJob(StatusFailure, true), newJob(StatusSkipped, false)}, + want: StatusSuccess, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, AggregateJobStatus(tt.jobs)) + }) + } +} diff --git a/models/actions/task.go b/models/actions/task.go index 36318c878a..047db27761 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -16,6 +16,7 @@ import ( "gitea.dev/models/db" "gitea.dev/models/unit" "gitea.dev/modules/actions/jobparser" + "gitea.dev/modules/globallock" "gitea.dev/modules/log" "gitea.dev/modules/setting" "gitea.dev/modules/timeutil" @@ -227,13 +228,23 @@ func makeTaskStepDisplayName(step *jobparser.Step, limit int) (name string) { return util.EllipsisDisplayString(name, limit) // database column has a length limit } -func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, false, err - } - defer committer.Close() +// errJobAlreadyClaimed is a sentinel used inside claimJobForRunner to signal that +// another runner won the optimistic-lock race; it is never returned to callers. +var errJobAlreadyClaimed = errors.New("job already claimed by another runner") +// pickTaskBatchSize bounds how many waiting jobs each CreateTaskForRunner query loads, +// so a large backlog is not fetched into memory on every runner poll. +// It is a var only so tests can shrink it to exercise pagination cheaply. +var pickTaskBatchSize = 100 + +// CreateTaskForRunner finds a waiting job that matches the runner's labels and +// atomically claims it. It iterates through all matching jobs so that a +// concurrent claim by another runner (which would lose the optimistic lock on +// job #1) does not leave the remaining jobs permanently unassigned. +func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) { + if db.InTransaction(ctx) { + return nil, false, errors.New("CreateTaskForRunner must not be called within a database transaction") + } e := db.GetEngine(ctx) jobCond := builder.NewCond() @@ -244,93 +255,174 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask Join("INNER", "repo_unit", "`repository`.id = `repo_unit`.repo_id"). Where(builder.Eq{"`repository`.owner_id": runner.OwnerID, "`repo_unit`.type": unit.TypeActions})) } - if jobCond.IsValid() { - jobCond = builder.In("run_id", builder.Select("id").From("action_run").Where(jobCond)) - } - - var jobs []*ActionRunJob - if err := e.Where("task_id=? AND status=? AND is_reusable_caller=?", 0, StatusWaiting, false).And(jobCond).Asc("updated", "id").Find(&jobs); err != nil { - return nil, false, err - } + baseCond := builder.Eq{"task_id": 0, "status": StatusWaiting, "is_reusable_caller": false}.And(jobCond) // TODO: a more efficient way to filter labels - var job *ActionRunJob log.Trace("runner labels: %v", runner.AgentLabels) - for _, v := range jobs { - if runner.CanMatchLabels(v.RunsOn) { - job = v - break + + // Page through the waiting jobs oldest-first instead of loading the whole backlog into memory on every poll. + // Keyset pagination on (updated, id) is safe under concurrent claims: + // updated only moves forward, so the advancing cursor never skips a still-waiting job even as claimed jobs drop out. + var cursorUpdated timeutil.TimeStamp + var cursorID int64 + for { + cond := baseCond + if cursorID > 0 { + cond = cond.And(builder.Or( + builder.Gt{"updated": cursorUpdated}, + builder.And(builder.Eq{"updated": cursorUpdated}, builder.Gt{"id": cursorID}), + )) } - } - if job == nil { - return nil, false, nil - } - if err := job.LoadAttributes(ctx); err != nil { - return nil, false, err - } - now := timeutil.TimeStampNow() - job.Started = now - job.Status = StatusRunning - - task := &ActionTask{ - JobID: job.ID, - Attempt: job.Attempt, - RunnerID: runner.ID, - Started: now, - Status: StatusRunning, - RepoID: job.RepoID, - OwnerID: job.OwnerID, - CommitSHA: job.CommitSHA, - IsForkPullRequest: job.IsForkPullRequest, - } - task.GenerateAndFillToken() - - workflowJob, err := job.ParseJob() - if err != nil { - return nil, false, fmt.Errorf("load job %d: %w", job.ID, err) - } - - if _, err := e.Insert(task); err != nil { - return nil, false, err - } - - task.LogFilename = logFileName(job.Run.Repo.FullName(), task.ID) - if err := UpdateTask(ctx, task, "log_filename"); err != nil { - return nil, false, err - } - - if len(workflowJob.Steps) > 0 { - steps := make([]*ActionTaskStep, len(workflowJob.Steps)) - for i, v := range workflowJob.Steps { - steps[i] = &ActionTaskStep{ - Name: makeTaskStepDisplayName(v, 255), - TaskID: task.ID, - Index: int64(i), - RepoID: task.RepoID, - Status: StatusWaiting, - } - } - if _, err := e.Insert(steps); err != nil { + var jobs []*ActionRunJob + if err := e.Where(cond).Asc("updated", "id").Limit(pickTaskBatchSize).Find(&jobs); err != nil { return nil, false, err } - task.Steps = steps - } - job.TaskID = task.ID - if n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}); err != nil { - return nil, false, err - } else if n != 1 { + for _, v := range jobs { + if !runner.CanMatchLabels(v.RunsOn) { + continue + } + task, ok, err := claimJobForRunner(ctx, runner, v) + if err != nil { + return nil, false, err + } + if ok { + return task, true, nil + } + // Another runner claimed this job concurrently; try the next one. + } + + // A short page means no waiting jobs remain beyond it. + if len(jobs) < pickTaskBatchSize { + return nil, false, nil + } + last := jobs[len(jobs)-1] + cursorUpdated, cursorID = last.Updated, last.ID + } +} + +// claimJobForRunner attempts to atomically claim job for runner inside its own +// transaction. Returns (task, true, nil) on success, or (nil, false, nil) when +// another runner wins the optimistic-lock race (the caller should try the next +// candidate job). +func claimJobForRunner(ctx context.Context, runner *ActionRunner, job *ActionRunJob) (*ActionTask, bool, error) { + var resultTask *ActionTask + + err := db.WithTx(ctx, func(ctx context.Context) error { + e := db.GetEngine(ctx) + + if err := job.LoadAttributes(ctx); err != nil { + return err + } + + now := timeutil.TimeStampNow() + job.Started = now + job.Status = StatusRunning + + task := &ActionTask{ + JobID: job.ID, + Attempt: job.Attempt, + RunnerID: runner.ID, + Started: now, + Status: StatusRunning, + RepoID: job.RepoID, + OwnerID: job.OwnerID, + CommitSHA: job.CommitSHA, + IsForkPullRequest: job.IsForkPullRequest, + } + task.GenerateAndFillToken() + + workflowJob, err := job.ParseJob() + if err != nil { + return fmt.Errorf("load job %d: %w", job.ID, err) + } + + if _, err := e.Insert(task); err != nil { + return err + } + + task.LogFilename = logFileName(job.Run.Repo.FullName(), task.ID) + if err := UpdateTask(ctx, task, "log_filename"); err != nil { + return err + } + + if len(workflowJob.Steps) > 0 { + steps := make([]*ActionTaskStep, len(workflowJob.Steps)) + for i, v := range workflowJob.Steps { + steps[i] = &ActionTaskStep{ + Name: makeTaskStepDisplayName(v, 255), + TaskID: task.ID, + Index: int64(i), + RepoID: task.RepoID, + Status: StatusWaiting, + } + } + if _, err := e.Insert(steps); err != nil { + return err + } + task.Steps = steps + } + + job.TaskID = task.ID + n, err := UpdateRunJob(ctx, job, builder.And(builder.Eq{"task_id": 0}, builder.Eq{"status": StatusWaiting})) + if err != nil { + return err + } + if n != 1 { + // Another runner claimed this job between our scan and this update; + // signal the outer loop to move on without treating this as an error. + return errJobAlreadyClaimed + } + + task.Job = job + resultTask = task + return nil + }) + + if errors.Is(err, errJobAlreadyClaimed) { return nil, false, nil } - - task.Job = job - - if err := committer.Commit(); err != nil { + if err != nil { return nil, false, err } + return resultTask, true, nil +} - return task, true, nil +// ReleaseTaskForRunner reverts a freshly-claimed but undelivered task: it deletes +// the task together with its steps and returns the job to the waiting queue. It is +// used when assembling the runner response fails after the job was already claimed, +// so the job is not stranded in running state with no runner ever executing it. +func ReleaseTaskForRunner(ctx context.Context, task *ActionTask) error { + return db.WithTx(ctx, func(ctx context.Context) error { + e := db.GetEngine(ctx) + + job, err := GetRunJobByRepoAndID(ctx, task.RepoID, task.JobID) + if err != nil { + return err + } + + job.Status = StatusWaiting + job.Started = 0 + job.TaskID = 0 + // Guard on task_id and status so we only release while the job still + // references this task and has not progressed past running. + n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": task.ID, "status": StatusRunning}, "status", "started", "task_id") + if err != nil { + return err + } + if n != 1 { + return fmt.Errorf("release task %d: job %d no longer references it", task.ID, task.JobID) + } + + if _, err := e.Delete(&ActionTaskStep{TaskID: task.ID}); err != nil { + return err + } + if _, err := e.ID(task.ID).Delete(&ActionTask{}); err != nil { + return err + } + return nil + }) } func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error { @@ -347,6 +439,18 @@ func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error { return err } +func getRunIDByTaskID(ctx context.Context, taskID int64) (runID int64, _ error) { + if has, err := db.GetEngine(ctx).Cols("action_run_job.run_id"). + Table("action_task"). + Join("INNER", "action_run_job", "action_run_job.id = action_task.job_id"). + Where(builder.Eq{"action_task.id": taskID}).Get(&runID); err != nil { + return runID, err + } else if !has { + return runID, util.ErrNotExist + } + return runID, nil +} + // UpdateTaskByState updates the task by the state. // It will always update the task if the state is not final, even there is no change. // So it will update ActionTask.Updated to avoid the task being judged as a zombie task. @@ -356,21 +460,26 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task stepStates[v.Id] = v } - return db.WithTx2(ctx, func(ctx context.Context) (*ActionTask, error) { - e := db.GetEngine(ctx) - - task := &ActionTask{} - if has, err := e.ID(state.Id).Get(task); err != nil { - return nil, err + // Only one request can update the task because the final state needs to be calculated with all job states. + // Otherwise, concurrent requests with transaction will make the SQL read stale job state and result in wrong final state. + taskID := state.Id + runID, err := getRunIDByTaskID(ctx, taskID) + if err != nil { + return nil, err + } + task := &ActionTask{} + err = globallock.LockAndDo(ctx, fmt.Sprintf("UpdateTaskByState-run-%d", runID), func(ctx context.Context) error { + if has, err := db.GetEngine(ctx).ID(taskID).Get(task); err != nil { + return err } else if !has { - return nil, util.ErrNotExist + return util.ErrNotExist } else if runnerID != task.RunnerID { - return nil, errors.New("invalid runner for task") + return errors.New("invalid runner for task") } if task.Status.IsDone() { // the state is final, do nothing - return task, nil + return nil } // state.Result is not unspecified means the task is finished @@ -383,7 +492,7 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task } task.Stopped = timeutil.TimeStamp(state.StoppedAt.AsTime().Unix()) if err := UpdateTask(ctx, task, "status", "stopped"); err != nil { - return nil, err + return err } if _, err := UpdateRunJob(ctx, &ActionRunJob{ ID: task.JobID, @@ -391,18 +500,18 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task Status: task.Status, Stopped: task.Stopped, }, nil, "status", "stopped"); err != nil { - return nil, err + return err } } else { // Force update ActionTask.Updated to avoid the task being judged as a zombie task task.Updated = timeutil.TimeStampNow() if err := UpdateTask(ctx, task, "updated"); err != nil { - return nil, err + return err } } if err := task.LoadAttributes(ctx); err != nil { - return nil, err + return err } for _, step := range task.Steps { @@ -419,13 +528,13 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task } else if step.Started != 0 { step.Status = StatusRunning } - if _, err := e.ID(step.ID).Update(step); err != nil { - return nil, err + if _, err := db.GetEngine(ctx).ID(step.ID).Update(step); err != nil { + return err } } - - return task, nil + return nil }) + return task, err } func StopTask(ctx context.Context, taskID int64, status Status) error { diff --git a/models/actions/task_test.go b/models/actions/task_test.go index fe4436ec63..05f564f2b4 100644 --- a/models/actions/task_test.go +++ b/models/actions/task_test.go @@ -306,3 +306,139 @@ func TestStopTaskCancellingFallsBackForMissingRunner(t *testing.T) { assert.Equal(t, StatusCancelled, jobAfterStop.Status) assert.NotZero(t, jobAfterStop.Stopped) } + +// TestReleaseTaskForRunner verifies that releasing a freshly-claimed task returns +// its job to the waiting queue and deletes the task and its steps, so a failure +// while assembling the runner response cannot strand the job in running state. +func TestReleaseTaskForRunner(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + run := &ActionRun{ + Title: "release-task-test-run", + RepoID: 1, + OwnerID: 2, + WorkflowID: "test.yaml", + Index: 9902, + TriggerUserID: 2, + Ref: "refs/heads/main", + CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0", + Event: "push", + TriggerEvent: "push", + Status: StatusWaiting, + } + require.NoError(t, db.Insert(t.Context(), run)) + + job := &ActionRunJob{ + RunID: run.ID, + RepoID: run.RepoID, + OwnerID: run.OwnerID, + CommitSHA: run.CommitSHA, + Name: "release-job", + Attempt: 1, + JobID: "release-job", + Status: StatusWaiting, + RunsOn: []string{"ubuntu-latest"}, + WorkflowPayload: []byte("on: push\njobs:\n release-job:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n"), + } + require.NoError(t, db.Insert(t.Context(), job)) + + runner := &ActionRunner{ + UUID: "release-runner-uuid", + Name: "release-runner", + AgentLabels: []string{"ubuntu-latest"}, + } + runner.GenerateAndFillToken() + require.NoError(t, db.Insert(t.Context(), runner)) + + task, ok, err := CreateTaskForRunner(t.Context(), runner) + require.NoError(t, err) + require.True(t, ok) + require.NotNil(t, task) + + claimed := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID}) + require.Equal(t, StatusRunning, claimed.Status) + require.Equal(t, task.ID, claimed.TaskID) + + require.NoError(t, ReleaseTaskForRunner(t.Context(), task)) + + // Job is back in the waiting queue with no task assigned. + released := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID}) + assert.Equal(t, StatusWaiting, released.Status) + assert.Zero(t, released.TaskID) + assert.Zero(t, released.Started) + + // The task and its steps are gone. + unittest.AssertNotExistsBean(t, &ActionTask{ID: task.ID}) + unittest.AssertNotExistsBean(t, &ActionTaskStep{TaskID: task.ID}) +} + +// TestCreateTaskForRunnerPagination verifies that a job sitting beyond the first page is still claimed +func TestCreateTaskForRunnerPagination(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + defer func(orig int) { pickTaskBatchSize = orig }(pickTaskBatchSize) + pickTaskBatchSize = 2 + + run := &ActionRun{ + Title: "pagination-test-run", + RepoID: 1, + OwnerID: 2, + WorkflowID: "test.yaml", + Index: 9903, + TriggerUserID: 2, + Ref: "refs/heads/main", + CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0", + Event: "push", + TriggerEvent: "push", + Status: StatusWaiting, + } + require.NoError(t, db.Insert(t.Context(), run)) + + // Five waiting jobs the runner cannot run, then one it can. + // With a page size of 2 the matching job only appears on the third page. + for i := range 5 { + mismatch := &ActionRunJob{ + RunID: run.ID, + RepoID: run.RepoID, + OwnerID: run.OwnerID, + CommitSHA: run.CommitSHA, + Name: "mismatch-" + string(rune('a'+i)), + Attempt: 1, + JobID: "mismatch-" + string(rune('a'+i)), + Status: StatusWaiting, + RunsOn: []string{"windows-latest"}, + } + require.NoError(t, db.Insert(t.Context(), mismatch)) + } + + target := &ActionRunJob{ + RunID: run.ID, + RepoID: run.RepoID, + OwnerID: run.OwnerID, + CommitSHA: run.CommitSHA, + Name: "target-job", + Attempt: 1, + JobID: "target-job", + Status: StatusWaiting, + RunsOn: []string{"ubuntu-latest"}, + WorkflowPayload: []byte("on: push\njobs:\n target-job:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n"), + } + require.NoError(t, db.Insert(t.Context(), target)) + + runner := &ActionRunner{ + UUID: "pagination-runner-uuid", + Name: "pagination-runner", + AgentLabels: []string{"ubuntu-latest"}, + } + runner.GenerateAndFillToken() + require.NoError(t, db.Insert(t.Context(), runner)) + + task, ok, err := CreateTaskForRunner(t.Context(), runner) + require.NoError(t, err) + require.True(t, ok) + require.NotNil(t, task) + + claimed := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: target.ID}) + assert.Equal(t, StatusRunning, claimed.Status) + assert.Equal(t, task.ID, claimed.TaskID) +} diff --git a/models/activities/repo_activity.go b/models/activities/repo_activity.go index fd6a1630e5..bce46eea6a 100644 --- a/models/activities/repo_activity.go +++ b/models/activities/repo_activity.go @@ -73,7 +73,7 @@ func GetActivityStats(ctx context.Context, repo *repo_model.Repository, timeFrom } defer closer.Close() - code, err := gitRepo.GetCodeActivityStats(timeFrom, repo.DefaultBranch) + code, err := gitRepo.GetCodeActivityStats(ctx, timeFrom, repo.DefaultBranch) if err != nil { return nil, fmt.Errorf("FillFromGit: %w", err) } @@ -90,7 +90,7 @@ func GetActivityStatsTopAuthors(ctx context.Context, repo *repo_model.Repository } defer closer.Close() - code, err := gitRepo.GetCodeActivityStats(timeFrom, "") + code, err := gitRepo.GetCodeActivityStats(ctx, timeFrom, "") if err != nil { return nil, fmt.Errorf("FillFromGit: %w", err) } diff --git a/models/admin/task.go b/models/admin/task.go index be02137c90..32c9ffef44 100644 --- a/models/admin/task.go +++ b/models/admin/task.go @@ -18,6 +18,8 @@ import ( "gitea.dev/modules/structs" "gitea.dev/modules/timeutil" "gitea.dev/modules/util" + + "xorm.io/builder" ) // Task represents a task @@ -172,17 +174,13 @@ func (err ErrTaskDoesNotExist) Unwrap() error { // GetMigratingTask returns the migrating task by repo's id func GetMigratingTask(ctx context.Context, repoID int64) (*Task, error) { - task := Task{ - RepoID: repoID, - Type: structs.TaskTypeMigrateRepo, - } - has, err := db.GetEngine(ctx).Get(&task) + task, has, err := db.Get[Task](ctx, builder.Eq{"repo_id": repoID, "`type`": structs.TaskTypeMigrateRepo}) if err != nil { return nil, err } else if !has { - return nil, ErrTaskDoesNotExist{0, repoID, task.Type} + return nil, ErrTaskDoesNotExist{0, repoID, structs.TaskTypeMigrateRepo} } - return &task, nil + return task, nil } // CreateTask creates a task on database diff --git a/models/asymkey/ssh_key_parse.go b/models/asymkey/ssh_key_parse.go index d39ddf3c77..302ae09b84 100644 --- a/models/asymkey/ssh_key_parse.go +++ b/models/asymkey/ssh_key_parse.go @@ -36,7 +36,18 @@ import ( const ssh2keyStart = "---- BEGIN SSH2 PUBLIC KEY ----" +const ( + // the longest RSA key ssh-keygen allows to generate is 16384 bits (2048 bytes), we still relax the limit a little here + maxKeyBinaryBytes = 4096 + maxKeyContentBase64Bytes = maxKeyBinaryBytes * 4 / 3 + maxKeyContentExtraBytes = 4 * 1024 // header, footer, comment + maxKeyContentBytes = maxKeyContentBase64Bytes + maxKeyContentExtraBytes +) + func extractTypeFromBase64Key(key string) (string, error) { + if len(key) > maxKeyContentBase64Bytes { + return "", util.NewInvalidArgumentErrorf("SSH public key base64 is too long") + } b, err := base64.StdEncoding.DecodeString(key) if err != nil || len(b) < 4 { return "", fmt.Errorf("invalid key format: %w", err) @@ -52,6 +63,10 @@ func extractTypeFromBase64Key(key string) (string, error) { // parseKeyString parses any key string in OpenSSH or SSH2 format to clean OpenSSH string (RFC4253). func parseKeyString(content string) (string, error) { + if len(content) > maxKeyContentBytes { + return "", util.NewInvalidArgumentErrorf("SSH public key content is too long") + } + // remove whitespace at start and end content = strings.TrimSpace(content) @@ -63,6 +78,8 @@ func parseKeyString(content string) (string, error) { // Transform all legal line endings to a single "\n". content = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(content) + var b strings.Builder + b.Grow(len(content)) lines := strings.Split(content, "\n") continuationLine := false @@ -74,9 +91,10 @@ func parseKeyString(content string) (string, error) { if continuationLine || strings.ContainsAny(line, ":-") { continuationLine = strings.HasSuffix(line, "\\") } else { - keyContent += line + b.WriteString(line) } } + keyContent = b.String() t, err := extractTypeFromBase64Key(keyContent) if err != nil { diff --git a/models/asymkey/ssh_key_test.go b/models/asymkey/ssh_key_test.go index 8ecb3f9474..8700be3c86 100644 --- a/models/asymkey/ssh_key_test.go +++ b/models/asymkey/ssh_key_test.go @@ -473,10 +473,20 @@ func runErr(t *testing.T, stdin []byte, args ...string) { } } -func Test_PublicKeysAreExternallyManaged(t *testing.T) { +func TestPublicKeysAreExternallyManaged(t *testing.T) { key1 := unittest.AssertExistsAndLoadBean(t, &PublicKey{ID: 1}) externals, err := PublicKeysAreExternallyManaged(t.Context(), []*PublicKey{key1}) assert.NoError(t, err) assert.Len(t, externals, 1) assert.False(t, externals[0]) } + +// TestCheckPublicKeyStringOversized tests if oversized SSH2 public key strings are rejected before triggering costly operations. +func TestCheckPublicKeyStringOversized(t *testing.T) { + _, err := parseKeyString(strings.Repeat("a", maxKeyContentBytes+1)) + assert.ErrorContains(t, err, "SSH public key content is too long") + + content := "---- BEGIN SSH2 PUBLIC KEY ----\n" + strings.Repeat("a", maxKeyContentBase64Bytes+1) + "\n--- END SSH2 PUBLIC KEY ----" + _, err = parseKeyString(content) + assert.ErrorContains(t, err, "SSH public key base64 is too long") +} diff --git a/models/auth/access_token_scope.go b/models/auth/access_token_scope.go index 71a81425a0..94833f1874 100644 --- a/models/auth/access_token_scope.go +++ b/models/auth/access_token_scope.go @@ -304,6 +304,36 @@ func (s AccessTokenScope) PublicOnly() (bool, error) { return bitmap.hasScope(AccessTokenScopePublicOnly) } +// CanCreateChildScope reports whether a request authenticated by this (parent) scope may mint a token +// carrying the child scope. It rejects any grantable scope the parent does not hold, closing the +// scope-escalation path. public-only is a restriction rather than a grantable permission, so it is +// ignored here (a child may always be public-only); EnforcePublicOnlyFrom handles carrying it down. +func (s AccessTokenScope) CanCreateChildScope(child AccessTokenScope) (bool, error) { + requested := child.StringSlice() + scopes := make([]AccessTokenScope, 0, len(requested)) + for _, sc := range requested { + childScope := AccessTokenScope(sc) + if childScope == AccessTokenScopePublicOnly { + continue + } + scopes = append(scopes, childScope) + } + return s.HasScope(scopes...) +} + +// EnforcePublicOnlyFrom adds the public-only restriction to s when the authorizing parent scope is +// public-only, so a public-only token cannot mint a child token that drops the restriction. +func (s AccessTokenScope) EnforcePublicOnlyFrom(parent AccessTokenScope) (AccessTokenScope, error) { + publicOnly, err := parent.PublicOnly() + if err != nil { + return "", err + } + if !publicOnly { + return s, nil + } + return AccessTokenScope(string(s) + "," + string(AccessTokenScopePublicOnly)).Normalize() +} + // HasScope returns true if the string has the given scope func (s AccessTokenScope) HasScope(scopes ...AccessTokenScope) (bool, error) { bitmap, err := s.parse() diff --git a/models/auth/access_token_scope_test.go b/models/auth/access_token_scope_test.go index b93c25528f..a54284e45f 100644 --- a/models/auth/access_token_scope_test.go +++ b/models/auth/access_token_scope_test.go @@ -89,3 +89,26 @@ func TestAccessTokenScope_HasScope(t *testing.T) { }) } } + +func TestAccessTokenScope_EnforcePublicOnlyFrom(t *testing.T) { + tests := []struct { + in AccessTokenScope + parent AccessTokenScope + out AccessTokenScope + }{ + // public-only parent forces the restriction onto the minted scope + {"write:user", "write:user,public-only", "public-only,write:user"}, + // already public-only stays public-only + {"public-only,read:user", "public-only", "public-only,read:user"}, + // non-public-only parent leaves the scope untouched + {"write:user", "write:user", "write:user"}, + {"all", "all", "all"}, + } + for _, test := range tests { + t.Run(string(test.parent)+"->"+string(test.in), func(t *testing.T) { + got, err := test.in.EnforcePublicOnlyFrom(test.parent) + assert.NoError(t, err) + assert.Equal(t, test.out, got) + }) + } +} diff --git a/models/auth/twofactor.go b/models/auth/twofactor.go index 51f487aac1..0362879e7e 100644 --- a/models/auth/twofactor.go +++ b/models/auth/twofactor.go @@ -195,3 +195,18 @@ func HasTwoFactorOrWebAuthn(ctx context.Context, id int64) (bool, error) { } return HasWebAuthnRegistrationsByUID(ctx, id) } + +// DisableTwoFactor removes every two-factor method of the given user atomically, +// returning the number of TOTP records and WebAuthn credentials removed. +// It is a no-op for a user that has no 2FA enrolled. +func DisableTwoFactor(ctx context.Context, uid int64) (totp, webAuthn int64, err error) { + err = db.WithTx(ctx, func(ctx context.Context) error { + var e error + if totp, e = db.GetEngine(ctx).Where("uid = ?", uid).Delete(&TwoFactor{}); e != nil { + return e + } + webAuthn, e = db.GetEngine(ctx).Where("user_id = ?", uid).Delete(&WebAuthnCredential{}) + return e + }) + return totp, webAuthn, err +} diff --git a/models/auth/twofactor_test.go b/models/auth/twofactor_test.go index 1da5814e03..835e7c9c41 100644 --- a/models/auth/twofactor_test.go +++ b/models/auth/twofactor_test.go @@ -10,6 +10,7 @@ import ( auth_model "gitea.dev/models/auth" "gitea.dev/models/unittest" + "github.com/go-webauthn/webauthn/webauthn" "github.com/pquerna/otp/totp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -45,3 +46,37 @@ func TestTwoFactorValidateAndConsumeTOTP(t *testing.T) { require.NoError(t, err) assert.False(t, ok) } + +func TestDisableTwoFactor(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + ctx := t.Context() + + const uid = 1000 // a uid with no user/2FA fixtures + + // Enroll TOTP and register a WebAuthn credential. + tfa := &auth_model.TwoFactor{UID: uid} + require.NoError(t, tfa.SetSecret("test-secret")) + require.NoError(t, auth_model.NewTwoFactor(ctx, tfa)) + _, err := auth_model.CreateCredential(ctx, uid, "test-key", &webauthn.Credential{ID: []byte("test-cred-id")}) + require.NoError(t, err) + + has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, uid) + require.NoError(t, err) + require.True(t, has) + + // Both records are removed and counted separately. + totp, webAuthn, err := auth_model.DisableTwoFactor(ctx, uid) + require.NoError(t, err) + assert.EqualValues(t, 1, totp) + assert.EqualValues(t, 1, webAuthn) + + has, err = auth_model.HasTwoFactorOrWebAuthn(ctx, uid) + require.NoError(t, err) + assert.False(t, has) + + // A second call on a user without 2FA is a no-op. + totp, webAuthn, err = auth_model.DisableTwoFactor(ctx, uid) + require.NoError(t, err) + assert.EqualValues(t, 0, totp) + assert.EqualValues(t, 0, webAuthn) +} diff --git a/models/avatars/avatar.go b/models/avatars/avatar.go index 7ecb55183c..59300103a7 100644 --- a/models/avatars/avatar.go +++ b/models/avatars/avatar.go @@ -20,6 +20,7 @@ import ( "gitea.dev/modules/setting" "strk.kbt.io/projects/go/libravatar" + "xorm.io/builder" ) const ( @@ -99,12 +100,13 @@ func HashEmail(email string) string { // GetEmailForHash converts a provided md5sum to the email func GetEmailForHash(ctx context.Context, md5Sum string) (string, error) { return cache.GetString("Avatar:"+md5Sum, func() (string, error) { - emailHash := EmailHash{ - Hash: strings.ToLower(strings.TrimSpace(md5Sum)), + emailHash, has, err := db.Get[EmailHash](ctx, builder.Eq{"`hash`": strings.ToLower(strings.TrimSpace(md5Sum))}) + if err != nil { + return "", err + } else if !has { + return "", nil } - - _, err := db.GetEngine(ctx).Get(&emailHash) - return emailHash.Email, err + return emailHash.Email, nil }) } diff --git a/models/git/branch.go b/models/git/branch.go index 17433fcff8..b74c7aa8b0 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -323,13 +323,7 @@ type RenamedBranch struct { // FindRenamedBranch check if a branch was renamed func FindRenamedBranch(ctx context.Context, repoID int64, from string) (branch *RenamedBranch, exist bool, err error) { - branch = &RenamedBranch{ - RepoID: repoID, - From: from, - } - exist, err = db.GetEngine(ctx).Get(branch) - - return branch, exist, err + return db.Get[RenamedBranch](ctx, builder.Eq{"repo_id": repoID, "`from`": from}) } // RenameBranch rename a branch diff --git a/models/git/commit_status_test.go b/models/git/commit_status_test.go index bb0858c44e..ca6402be7d 100644 --- a/models/git/commit_status_test.go +++ b/models/git/commit_status_test.go @@ -186,11 +186,11 @@ func TestFindRepoRecentCommitStatusContexts(t *testing.T) { repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - gitRepo, err := gitrepo.OpenRepository(t.Context(), repo2) + gitRepo, err := gitrepo.OpenRepository(repo2) assert.NoError(t, err) defer gitRepo.Close() - commit, err := gitRepo.GetBranchCommit(repo2.DefaultBranch) + commit, err := gitRepo.GetBranchCommit(t.Context(), repo2.DefaultBranch) assert.NoError(t, err) defer func() { diff --git a/models/git/lfs.go b/models/git/lfs.go index 75abb89f1f..c5018cb398 100644 --- a/models/git/lfs.go +++ b/models/git/lfs.go @@ -126,8 +126,7 @@ func GetLFSMetaObjectByOid(ctx context.Context, repoID int64, oid string) (*LFSM return nil, ErrLFSObjectNotExist } - m := &LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}, RepositoryID: repoID} - has, err := db.GetEngine(ctx).Get(m) + m, has, err := db.Get[LFSMetaObject](ctx, builder.Eq{"repository_id": repoID, "oid": oid}) if err != nil { return nil, err } else if !has { diff --git a/models/git/protected_tag.go b/models/git/protected_tag.go index 35d406f45c..3af2c5c769 100644 --- a/models/git/protected_tag.go +++ b/models/git/protected_tag.go @@ -13,6 +13,8 @@ import ( "gitea.dev/models/organization" "gitea.dev/modules/glob" "gitea.dev/modules/timeutil" + + "xorm.io/builder" ) // ProtectedTag struct @@ -111,8 +113,7 @@ func GetProtectedTagByID(ctx context.Context, id int64) (*ProtectedTag, error) { // GetProtectedTagByNamePattern gets protected tag by name_pattern func GetProtectedTagByNamePattern(ctx context.Context, repoID int64, pattern string) (*ProtectedTag, error) { - tag := &ProtectedTag{NamePattern: pattern, RepoID: repoID} - has, err := db.GetEngine(ctx).Get(tag) + tag, has, err := db.Get[ProtectedTag](ctx, builder.Eq{"name_pattern": pattern, "repo_id": repoID}) if err != nil { return nil, err } diff --git a/models/issues/comment.go b/models/issues/comment.go index febc49ca76..40fa999750 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -27,6 +27,7 @@ import ( "gitea.dev/modules/markup" "gitea.dev/modules/optional" "gitea.dev/modules/references" + "gitea.dev/modules/setting" "gitea.dev/modules/structs" "gitea.dev/modules/timeutil" "gitea.dev/modules/translation" @@ -626,11 +627,18 @@ func UpdateCommentAttachments(ctx context.Context, c *Comment, uuids []string) e return nil } return db.WithTx(ctx, func(ctx context.Context) error { + issue, err := GetIssueByID(ctx, c.IssueID) + if err != nil { + return err + } attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) if err != nil { return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) } for i := range attachments { + if err := validateAttachmentForIssue(ctx, issue, attachments[i]); err != nil { + return err + } attachments[i].IssueID = c.IssueID attachments[i].CommentID = c.ID if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { @@ -643,36 +651,18 @@ func UpdateCommentAttachments(ctx context.Context, c *Comment, uuids []string) e } // LoadAssigneeUserAndTeam if comment.Type is CommentTypeAssignees, then load assignees -func (c *Comment) LoadAssigneeUserAndTeam(ctx context.Context) error { - var err error - +func (c *Comment) LoadAssigneeUserAndTeam(ctx context.Context) (err error) { if c.AssigneeID > 0 && c.Assignee == nil { - c.Assignee, err = user_model.GetUserByID(ctx, c.AssigneeID) + _, c.Assignee, err = user_model.GetPossibleUserByID(ctx, c.AssigneeID) if err != nil { - if !user_model.IsErrUserNotExist(err) { - return err - } - c.Assignee = user_model.NewGhostUser() - } - } else if c.AssigneeTeamID > 0 && c.AssigneeTeam == nil { - if err = c.LoadIssue(ctx); err != nil { return err } - - if err = c.Issue.LoadRepo(ctx); err != nil { + } + if c.AssigneeTeamID > 0 && c.AssigneeTeam == nil { + _, c.AssigneeTeam, err = organization.GetPossibleTeamByID(ctx, c.AssigneeTeamID) + if err != nil { return err } - - if err = c.Issue.Repo.LoadOwner(ctx); err != nil { - return err - } - - if c.Issue.Repo.Owner.IsOrganization() { - c.AssigneeTeam, err = organization.GetTeamByID(ctx, c.AssigneeTeamID) - if err != nil && !organization.IsErrTeamNotExist(err) { - return err - } - } } return nil } @@ -795,8 +785,7 @@ func (c *Comment) MetaSpecialDoerTr(locale translation.Locale) template.HTML { } func (c *Comment) TimelineRequestedReviewTr(locale translation.Locale, createdStr template.HTML) template.HTML { - if c.AssigneeID > 0 { - // it guarantees LoadAssigneeUserAndTeam has been called, and c.Assignee is Ghost user but not nil if the user doesn't exist + if c.Assignee != nil { if c.RemovedAssignee { if c.PosterID == c.AssigneeID { return locale.Tr("repo.issues.review.remove_review_request_self", createdStr) @@ -805,14 +794,20 @@ func (c *Comment) TimelineRequestedReviewTr(locale translation.Locale, createdSt } return locale.Tr("repo.issues.review.add_review_request", c.Assignee.GetDisplayName(), createdStr) } - teamName := "Ghost Team" if c.AssigneeTeam != nil { - teamName = c.AssigneeTeam.Name + if c.RemovedAssignee { + return locale.Tr("repo.issues.review.remove_review_request", c.AssigneeTeam.Name, createdStr) + } + return locale.Tr("repo.issues.review.add_review_request", c.AssigneeTeam.Name, createdStr) } + + // impossible fallback + assigneePrompt := fmt.Sprintf("(AssigneeID=%d, AssigneeTeamID=%d)", c.AssigneeID, c.AssigneeTeam.ID) + setting.PanicInDevOrTesting("unknown timeline pull request review event comment: id=%d, %s", c.ID, assigneePrompt) if c.RemovedAssignee { - return locale.Tr("repo.issues.review.remove_review_request", teamName, createdStr) + return locale.Tr("repo.issues.review.remove_review_request", assigneePrompt, createdStr) } - return locale.Tr("repo.issues.review.add_review_request", teamName, createdStr) + return locale.Tr("repo.issues.review.add_review_request", assigneePrompt, createdStr) } // CreateComment creates comment with context diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go index 7195736f92..f6cebe37b0 100644 --- a/models/issues/comment_code.go +++ b/models/issues/comment_code.go @@ -79,6 +79,14 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu return nil, err } + if err := comments.loadResolveDoers(ctx); err != nil { + return nil, err + } + + if err := comments.loadReactions(ctx, issue.Repo); err != nil { + return nil, err + } + // Find all reviews by ReviewID reviews := make(map[int64]*Review) ids := make([]int64, 0, len(comments)) @@ -107,14 +115,6 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu comments[n] = comment n++ - if err := comment.LoadResolveDoer(ctx); err != nil { - return nil, err - } - - if err := comment.LoadReactions(ctx, issue.Repo); err != nil { - return nil, err - } - var err error rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{ FootnoteContextID: strconv.FormatInt(comment.ID, 10), diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go index 522c066309..610bd16f5a 100644 --- a/models/issues/comment_list.go +++ b/models/issues/comment_list.go @@ -11,6 +11,7 @@ import ( user_model "gitea.dev/models/user" "gitea.dev/modules/container" "gitea.dev/modules/log" + "gitea.dev/modules/setting" ) // CommentList defines a list of comments @@ -444,6 +445,73 @@ func (comments CommentList) loadReviews(ctx context.Context) error { return nil } +// loadResolveDoers bulk-loads the resolve doer for all code comments that have one. +func (comments CommentList) loadResolveDoers(ctx context.Context) error { + resolveDoerIDs := container.FilterSlice(comments, func(c *Comment) (int64, bool) { + return c.ResolveDoerID, c.ResolveDoerID != 0 && c.Type == CommentTypeCode + }) + if len(resolveDoerIDs) == 0 { + return nil + } + + userMaps, err := user_model.GetUsersMapByIDs(ctx, resolveDoerIDs) + if err != nil { + return err + } + + for _, comment := range comments { + if comment.ResolveDoerID == 0 || comment.Type != CommentTypeCode { + continue + } + if u, ok := userMaps[comment.ResolveDoerID]; ok { + comment.ResolveDoer = u + } else { + comment.ResolveDoer = user_model.NewGhostUser() + } + } + return nil +} + +// loadReactions bulk-loads reactions for all comments in the list. +func (comments CommentList) loadReactions(ctx context.Context, repo *repo_model.Repository) error { + if len(comments) == 0 { + return nil + } + + commentIDs := container.FilterSlice(comments, func(c *Comment) (int64, bool) { + return c.ID, c.Reactions == nil + }) + if len(commentIDs) == 0 { + return nil + } + + var allReactions ReactionList + if err := db.GetEngine(ctx). + Where("`comment_id` > 0"). + In("comment_id", commentIDs). + In("`type`", setting.UI.Reactions). + Asc("issue_id", "comment_id", "created_unix", "id"). + Find(&allReactions); err != nil { + return err + } + + if _, err := allReactions.LoadUsers(ctx, repo); err != nil { + return err + } + + reactByComment := make(map[int64]ReactionList, len(commentIDs)) + for _, r := range allReactions { + reactByComment[r.CommentID] = append(reactByComment[r.CommentID], r) + } + + for _, comment := range comments { + if comment.Reactions == nil { + comment.Reactions = reactByComment[comment.ID] + } + } + return nil +} + // LoadAttributes loads attributes of the comments, except for attachments and // comments func (comments CommentList) LoadAttributes(ctx context.Context) (err error) { diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index c47a0c7a4a..7b4dfed083 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -45,12 +45,27 @@ func TestCreateComment(t *testing.T) { unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix)) } +func TestLoadAssigneeUserAndTeam_DeletedTeamBecomesGhostTeam(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 15}) + comment := &issues_model.Comment{ + Type: issues_model.CommentTypeAssignees, + IssueID: issue.ID, + AssigneeTeamID: 999999, // non-existing team ID + } + assert.NoError(t, comment.LoadAssigneeUserAndTeam(t.Context())) + assert.NotNil(t, comment.AssigneeTeam) + assert.EqualValues(t, -1, comment.AssigneeTeam.ID) +} + func Test_UpdateCommentAttachment(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1}) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}) attachment := repo_model.Attachment{ - Name: "test.txt", + RepoID: issue.RepoID, // must match the comment's repo, else the cross-repo guard rejects it + Name: "test.txt", } assert.NoError(t, db.Insert(t.Context(), &attachment)) diff --git a/models/issues/issue.go b/models/issues/issue.go index 1dba275ff0..a0af16db83 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -361,8 +361,8 @@ func (issue *Issue) ResetAttributesLoaded() { // GetIsRead load the `IsRead` field of the issue func (issue *Issue) GetIsRead(ctx context.Context, userID int64) error { - issueUser := &IssueUser{IssueID: issue.ID, UID: userID} - if has, err := db.GetEngine(ctx).Get(issueUser); err != nil { + issueUser, has, err := db.Get[IssueUser](ctx, builder.Eq{"issue_id": issue.ID, "uid": userID}) + if err != nil { return err } else if !has { issue.IsRead = false @@ -499,11 +499,7 @@ func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) { if index < 1 { return nil, ErrIssueNotExist{} } - issue := &Issue{ - RepoID: repoID, - Index: index, - } - has, err := db.GetEngine(ctx).Get(issue) + issue, has, err := db.Get[Issue](ctx, builder.Eq{"repo_id": repoID, "`index`": index}) if err != nil { return nil, err } else if !has { diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index 45e25c2da8..250344dd66 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -591,9 +591,12 @@ func (issues IssueList) GetApprovalCounts(ctx context.Context) (map[int64][]*Rev func (issues IssueList) LoadIsRead(ctx context.Context, userID int64) error { issueIDs := issues.getIssueIDs() + if len(issueIDs) == 0 { + return nil + } issueUsers := make([]*IssueUser, 0, len(issueIDs)) if err := db.GetEngine(ctx).Where("uid =?", userID). - In("issue_id"). + In("issue_id", issueIDs). Find(&issueUsers); err != nil { return err } diff --git a/models/issues/issue_list_test.go b/models/issues/issue_list_test.go index 1c9f2a1e8e..bba864b4c5 100644 --- a/models/issues/issue_list_test.go +++ b/models/issues/issue_list_test.go @@ -11,6 +11,7 @@ import ( "gitea.dev/modules/setting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIssueList_LoadRepositories(t *testing.T) { @@ -30,6 +31,22 @@ func TestIssueList_LoadRepositories(t *testing.T) { } } +func TestIssueList_LoadIsRead(t *testing.T) { + // Regression: In("issue_id") was missing the issueIDs argument, causing + // xorm to generate "0=1" and never mark any issue as read. + require.NoError(t, unittest.PrepareTestDatabase()) + + issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + + // Fixture: uid=1 has is_read=true on issue 1 only. + issueList := issues_model.IssueList{issue1, issue2} + require.NoError(t, issueList.LoadIsRead(t.Context(), 1)) + + assert.True(t, issue1.IsRead, "issue 1 should be marked read for user 1") + assert.False(t, issue2.IsRead, "issue 2 should not be marked read for user 1") +} + func TestIssueList_LoadAttributes(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) setting.Service.EnableTimetracking = true diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 74a4b4aac1..646b261070 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -263,14 +263,46 @@ func AddDeletePRBranchComment(ctx context.Context, doer *user_model.User, repo * return err } +// validateAttachmentForIssue rejects a foreign or already-linked attachment before it is linked to +// issue: a known UUID could otherwise re-link (and expose) another repo's private attachment. A +// legacy attachment predating repo_id-on-upload is adopted into the issue's repo. +func validateAttachmentForIssue(ctx context.Context, issue *Issue, attachment *repo_model.Attachment) error { + if attachment.RepoID == 0 && attachment.CreatedUnix < repo_model.LegacyAttachmentMissingRepoIDCutoff { + attachment.RepoID = issue.RepoID + if err := repo_model.UpdateAttachmentByUUID(ctx, attachment, "repo_id"); err != nil { + return fmt.Errorf("update attachment repo_id [id: %d]: %w", attachment.ID, err) + } + } + if attachment.RepoID != issue.RepoID { + return util.NewPermissionDeniedErrorf("attachment belongs to a different repository") + } + if attachment.IssueID != 0 && attachment.IssueID != issue.ID { + return util.NewPermissionDeniedErrorf("attachment is already linked to another issue") + } + if attachment.ReleaseID != 0 { + return util.NewPermissionDeniedErrorf("attachment is already linked to a release") + } + return nil +} + // UpdateIssueAttachments update attachments by UUIDs for the issue func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string) (err error) { + if len(uuids) == 0 { + return nil + } return db.WithTx(ctx, func(ctx context.Context) error { + issue, err := GetIssueByID(ctx, issueID) + if err != nil { + return err + } attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) if err != nil { return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) } for i := range attachments { + if err := validateAttachmentForIssue(ctx, issue, attachments[i]); err != nil { + return err + } attachments[i].IssueID = issueID if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) @@ -599,7 +631,7 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true continue } - has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype}) + has, err := db.Exist[organization.TeamUnit](ctx, builder.Eq{"org_id": issue.Repo.Owner.ID, "team_id": team.ID, "`type`": unittype}) if err != nil { return nil, fmt.Errorf("get team units (%d): %w", team.ID, err) } diff --git a/models/issues/issue_update_test.go b/models/issues/issue_update_test.go new file mode 100644 index 0000000000..9048061e19 --- /dev/null +++ b/models/issues/issue_update_test.go @@ -0,0 +1,33 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issues_test + +import ( + "testing" + + issues_model "gitea.dev/models/issues" + repo_model "gitea.dev/models/repo" + "gitea.dev/models/unittest" + "gitea.dev/modules/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpdateIssueAttachmentsCrossRepo(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + // attachment id 2 belongs to repo 2 / issue 4; issue 1 lives in repo 1 + issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + foreign := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 2}) + require.NotEqual(t, issue1.RepoID, foreign.RepoID) + + // re-linking a foreign repo's attachment by UUID must be rejected + err := issues_model.UpdateIssueAttachments(t.Context(), issue1.ID, []string{foreign.UUID}) + assert.ErrorIs(t, err, util.ErrPermissionDenied) + + // the foreign attachment must be left untouched + reloaded := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 2}) + assert.Equal(t, foreign.IssueID, reloaded.IssueID) +} diff --git a/models/issues/issue_xref.go b/models/issues/issue_xref.go index 0bdc48d715..74f97f1ad5 100644 --- a/models/issues/issue_xref.go +++ b/models/issues/issue_xref.go @@ -13,6 +13,8 @@ import ( user_model "gitea.dev/models/user" "gitea.dev/modules/log" "gitea.dev/modules/references" + + "xorm.io/builder" ) type crossReference struct { @@ -189,11 +191,11 @@ func (issue *Issue) updateCrossReferenceList(list []*crossReference, xref *cross func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossReferencesContext, repo *repo_model.Repository, ref references.IssueReference, ) (*Issue, references.XRefAction, error) { - refIssue := &Issue{RepoID: repo.ID, Index: ref.Index} refAction := ref.Action - e := db.GetEngine(stdCtx) - - if has, _ := e.Get(refIssue); !has { + refIssue, has, err := db.Get[Issue](stdCtx, builder.Eq{"repo_id": repo.ID, "`index`": ref.Index}) + if err != nil { + return nil, references.XRefActionNone, err + } else if !has { return nil, references.XRefActionNone, nil } if err := refIssue.LoadRepo(stdCtx); err != nil { diff --git a/models/issues/label.go b/models/issues/label.go index e3312bdab1..5b9898b3b2 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -6,6 +6,7 @@ package issues import ( "context" + "errors" "fmt" "slices" "strconv" @@ -27,12 +28,6 @@ type ErrRepoLabelNotExist struct { RepoID int64 } -// IsErrRepoLabelNotExist checks if an error is a RepoErrLabelNotExist. -func IsErrRepoLabelNotExist(err error) bool { - _, ok := err.(ErrRepoLabelNotExist) - return ok -} - func (err ErrRepoLabelNotExist) Error() string { return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID) } @@ -312,6 +307,18 @@ func GetLabelInRepoByName(ctx context.Context, repoID int64, labelName string) ( return l, nil } +// GetLabelInRepoOrOrgByID returns the label with labelID scoped to the repo, falling back to the +// repo's owning organization when ownerIsOrg is set. It returns ErrRepoLabelNotExist / +// ErrOrgLabelNotExist when the label is in neither scope, so a foreign-but-existing label ID is +// indistinguishable from a nonexistent one (no cross-repo enumeration oracle). +func GetLabelInRepoOrOrgByID(ctx context.Context, repoID, ownerID int64, ownerIsOrg bool, labelID int64) (*Label, error) { + label, err := GetLabelInRepoByID(ctx, repoID, labelID) + if err != nil && errors.Is(err, util.ErrNotExist) && ownerIsOrg { + return GetLabelInOrgByID(ctx, ownerID, labelID) + } + return label, err +} + // GetLabelInRepoByID returns a label by ID in given repository. func GetLabelInRepoByID(ctx context.Context, repoID, labelID int64) (*Label, error) { if labelID <= 0 || repoID <= 0 { diff --git a/models/issues/label_test.go b/models/issues/label_test.go index f336190f0f..ea70e1cc21 100644 --- a/models/issues/label_test.go +++ b/models/issues/label_test.go @@ -12,6 +12,7 @@ import ( "gitea.dev/models/unittest" user_model "gitea.dev/models/user" "gitea.dev/modules/timeutil" + "gitea.dev/modules/util" "github.com/stretchr/testify/assert" ) @@ -94,10 +95,10 @@ func TestGetLabelInRepoByName(t *testing.T) { assert.Equal(t, "label1", label.Name) _, err = issues_model.GetLabelInRepoByName(t.Context(), 1, "") - assert.True(t, issues_model.IsErrRepoLabelNotExist(err)) + assert.ErrorIs(t, err, util.ErrNotExist) _, err = issues_model.GetLabelInRepoByName(t.Context(), unittest.NonexistentID, "nonexistent") - assert.True(t, issues_model.IsErrRepoLabelNotExist(err)) + assert.ErrorIs(t, err, util.ErrNotExist) } func TestGetLabelInRepoByNames(t *testing.T) { @@ -131,10 +132,10 @@ func TestGetLabelInRepoByID(t *testing.T) { assert.EqualValues(t, 1, label.ID) _, err = issues_model.GetLabelInRepoByID(t.Context(), 1, -1) - assert.True(t, issues_model.IsErrRepoLabelNotExist(err)) + assert.ErrorIs(t, err, util.ErrNotExist) _, err = issues_model.GetLabelInRepoByID(t.Context(), unittest.NonexistentID, unittest.NonexistentID) - assert.True(t, issues_model.IsErrRepoLabelNotExist(err)) + assert.ErrorIs(t, err, util.ErrNotExist) } func TestGetLabelsInRepoByIDs(t *testing.T) { diff --git a/models/issues/pull.go b/models/issues/pull.go index 6de5338a53..7eb0a5b72d 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -535,12 +535,8 @@ func GetPullRequestByIndex(ctx context.Context, repoID, index int64) (*PullReque if index < 1 { return nil, ErrPullRequestNotExist{} } - pr := &PullRequest{ - BaseRepoID: repoID, - Index: index, - } - has, err := db.GetEngine(ctx).Get(pr) + pr, has, err := db.Get[PullRequest](ctx, builder.Eq{"base_repo_id": repoID, "`index`": index}) if err != nil { return nil, err } else if !has { diff --git a/models/issues/review.go b/models/issues/review.go index e1f522be75..a051440290 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -324,6 +324,59 @@ func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organizatio return slices.Contains(pb.ApprovalsWhitelistTeamIDs, team.ID), nil } +// RecalculateReviewsOfficial re-evaluates the "official" flag of the latest approve +// and reject reviews of an issue against its pull request's current base branch. +// It must be called whenever the target branch changes, otherwise an approval that +// was official on the previous (possibly unprotected) branch would keep satisfying +// the new branch's protection rules. +func RecalculateReviewsOfficial(ctx context.Context, issue *Issue) error { + if err := issue.LoadPullRequest(ctx); err != nil { + return err + } + + // Clearing and restoring the official flags must happen atomically, otherwise a + // failure in between would leave the reviews without any official flag set. + return db.WithTx(ctx, func(ctx context.Context) error { + // Only the latest approve/reject review of each reviewer counts as official, so + // clear the flag on all of them first and restore it only where it still applies. + if _, err := db.GetEngine(ctx). + Where("issue_id = ?", issue.ID). + In("type", ReviewTypeApprove, ReviewTypeReject). + Cols("official"). + Update(&Review{Official: false}); err != nil { + return err + } + + reviews, err := FindLatestReviews(ctx, FindReviewOptions{ + Types: []ReviewType{ReviewTypeApprove, ReviewTypeReject}, + IssueID: issue.ID, + }) + if err != nil { + return err + } + + for _, review := range reviews { + if err := review.LoadReviewer(ctx); err != nil { + return err + } + if review.Reviewer == nil { + continue + } + official, err := IsOfficialReviewer(ctx, issue, review.Reviewer) + if err != nil { + return err + } + if official { + if _, err := db.GetEngine(ctx).ID(review.ID).Cols("official").Update(&Review{Official: true}); err != nil { + return err + } + } + } + + return nil + }) +} + // CreateReview creates a new review based on opts func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error) { return db.WithTx2(ctx, func(ctx context.Context) (*Review, error) { diff --git a/models/issues/review_test.go b/models/issues/review_test.go index 3d2672c403..6010f74c41 100644 --- a/models/issues/review_test.go +++ b/models/issues/review_test.go @@ -6,6 +6,8 @@ package issues_test import ( "testing" + "gitea.dev/models/db" + git_model "gitea.dev/models/git" issues_model "gitea.dev/models/issues" repo_model "gitea.dev/models/repo" "gitea.dev/models/unittest" @@ -386,3 +388,45 @@ func TestAddReviewRequest(t *testing.T) { assert.NotNil(t, comment.CommentMetaData) assert.Equal(t, issues_model.SpecialDoerNameCodeOwners, comment.CommentMetaData.SpecialDoerName) } + +func TestRecalculateReviewsOfficial(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // PR #2 targets repo1's "master" branch. Simulate an approval that became + // official while the PR targeted an unprotected branch. + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}) + reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + review, err := issues_model.CreateReview(t.Context(), issues_model.CreateReviewOptions{ + Type: issues_model.ReviewTypeApprove, + Issue: issue, + Reviewer: reviewer, + Official: true, + }) + assert.NoError(t, err) + + // Protect the (now current) target branch with an approvals whitelist that + // does not include the reviewer, mirroring a retarget onto a protected branch. + rule := &git_model.ProtectedBranch{ + RepoID: issue.RepoID, + RuleName: "master", + EnableApprovalsWhitelist: true, + ApprovalsWhitelistUserIDs: []int64{2}, + RequiredApprovals: 1, + } + assert.NoError(t, db.Insert(t.Context(), rule)) + + // Re-evaluating must strip the stale official flag, otherwise the approval + // would still satisfy the protected branch's required approvals. + assert.NoError(t, issues_model.RecalculateReviewsOfficial(t.Context(), issue)) + review = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: review.ID}) + assert.False(t, review.Official) + + // Once the reviewer is whitelisted, re-evaluating restores the official flag. + rule.ApprovalsWhitelistUserIDs = []int64{2, reviewer.ID} + _, err = db.GetEngine(t.Context()).ID(rule.ID).Cols("approvals_whitelist_user_i_ds").Update(rule) + assert.NoError(t, err) + + assert.NoError(t, issues_model.RecalculateReviewsOfficial(t.Context(), issue)) + review = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: review.ID}) + assert.True(t, review.Official) +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index a026ee7a52..f77ce03201 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -417,6 +417,9 @@ func prepareMigrationTasks() []*migration { newMigration(337, "Add visibility to team", v1_27.AddVisibilityToTeam), newMigration(338, "Expand legacy MSSQL issue/comment long-text columns", v1_27.ExpandIssueAndCommentLongTextFieldsForMSSQL), newMigration(339, "Extend action c_u index to include created_unix for faster dashboard feed queries", v1_27.AddCreatedUnixToActionUserIsDeletedIndex), + newMigration(340, "Add ContinueOnError column to ActionRunJob", v1_27.AddContinueOnErrorToActionRunJob), + newMigration(341, "Convert legacy MSSQL DATETIME columns to DATETIME2", v1_27.FixLegacyMSSQLDateTimeColumns), + newMigration(342, "Add scoped workflows schema", v1_27.AddScopedWorkflowsSchema), } return preparedMigrations } diff --git a/models/migrations/v1_12/v136.go b/models/migrations/v1_12/v136.go index 66f7b2aea4..76b882d7a2 100644 --- a/models/migrations/v1_12/v136.go +++ b/models/migrations/v1_12/v136.go @@ -83,7 +83,7 @@ func AddCommitDivergenceToPulls(x db.EngineMigration) error { log.Error("Missing base repo with id %d for PR ID %d", pr.BaseRepoID, pr.ID) continue } - repoStore := repo_model.StorageRepo(repo_model.RelativePath(baseRepo.OwnerName, baseRepo.Name)) + repoStore := repo_model.CodeRepoByName(baseRepo.OwnerName, baseRepo.Name) gitRefName := fmt.Sprintf("refs/pull/%d/head", pr.Index) divergence, err := gitrepo.GetDivergingCommits(graceful.GetManager().HammerContext(), repoStore, pr.BaseBranch, gitRefName) if err != nil { diff --git a/models/migrations/v1_14/v156.go b/models/migrations/v1_14/v156.go index e1078f4bd0..429269d991 100644 --- a/models/migrations/v1_14/v156.go +++ b/models/migrations/v1_14/v156.go @@ -6,24 +6,14 @@ package v1_14 import ( "context" "fmt" - "path/filepath" "strings" "gitea.dev/models/db" + repo_model "gitea.dev/models/repo" "gitea.dev/modules/git" "gitea.dev/modules/log" - "gitea.dev/modules/setting" ) -// Copy paste from models/repo.go because we cannot import models package -func repoPath(userName, repoName string) string { - return filepath.Join(userPath(userName), strings.ToLower(repoName)+".git") -} - -func userPath(userName string) string { - return filepath.Join(setting.RepoRootPath, strings.ToLower(userName)) -} - func FixPublisherIDforTagReleases(ctx context.Context, x db.EngineMigration) error { type Release struct { ID int64 @@ -108,14 +98,14 @@ func FixPublisherIDforTagReleases(ctx context.Context, x db.EngineMigration) err return err } } - gitRepo, err = git.OpenRepository(ctx, repoPath(repo.OwnerName, repo.Name)) + gitRepo, err = git.OpenRepository(repo_model.CodeRepoByName(repo.OwnerName, repo.Name)) if err != nil { log.Error("Error whilst opening git repo for [%d]%s/%s. Error: %v", repo.ID, repo.OwnerName, repo.Name, err) return err } } - commit, err := gitRepo.GetTagCommit(release.TagName) + commit, err := gitRepo.GetTagCommit(ctx, release.TagName) if err != nil { if git.IsErrNotExist(err) { log.Warn("Unable to find commit %s for Tag: %s in [%d]%s/%s. Cannot update publisher ID.", err.(git.ErrNotExist).ID, release.TagName, repo.ID, repo.OwnerName, repo.Name) @@ -127,7 +117,7 @@ func FixPublisherIDforTagReleases(ctx context.Context, x db.EngineMigration) err if commit.Author.Email == "" { log.Warn("Tag: %s in Repo[%d]%s/%s does not have a tagger.", release.TagName, repo.ID, repo.OwnerName, repo.Name) - commit, err = gitRepo.GetCommit(commit.ID.String()) + commit, err = gitRepo.GetCommit(ctx, commit.ID.String()) if err != nil { if git.IsErrNotExist(err) { log.Warn("Unable to find commit %s for Tag: %s in [%d]%s/%s. Cannot update publisher ID.", err.(git.ErrNotExist).ID, release.TagName, repo.ID, repo.OwnerName, repo.Name) diff --git a/models/migrations/v1_21/v276.go b/models/migrations/v1_21/v276.go index 1bd0157156..516cb9628a 100644 --- a/models/migrations/v1_21/v276.go +++ b/models/migrations/v1_21/v276.go @@ -159,12 +159,12 @@ func migratePushMirrors(x db.EngineMigration) error { func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) { ctx := context.Background() - relativePath := repo_model.RelativePath(ownerName, repoName) - if exist, _ := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(relativePath)); !exist { + repo := repo_model.CodeRepoByName(ownerName, repoName) + if exist, _ := gitrepo.IsRepositoryExist(ctx, repo); !exist { return "", nil } - u, err := gitrepo.GitRemoteGetURL(ctx, repo_model.StorageRepo(relativePath), remoteName) + u, err := gitrepo.GitRemoteGetURL(ctx, repo, remoteName) if err != nil { return "", err } diff --git a/models/migrations/v1_27/v340.go b/models/migrations/v1_27/v340.go new file mode 100644 index 0000000000..dcb942dfb4 --- /dev/null +++ b/models/migrations/v1_27/v340.go @@ -0,0 +1,24 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_27 + +import ( + "gitea.dev/models/db" + + "xorm.io/xorm" +) + +// AddContinueOnErrorToActionRunJob adds the ContinueOnError column to ActionRunJob, +// storing the job-level continue-on-error value from the workflow YAML. +func AddContinueOnErrorToActionRunJob(x db.EngineMigration) error { + type ActionRunJob struct { + ContinueOnError bool `xorm:"NOT NULL DEFAULT FALSE"` + } + + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + IgnoreConstrains: true, + }, new(ActionRunJob)) + return err +} diff --git a/models/migrations/v1_27/v341.go b/models/migrations/v1_27/v341.go new file mode 100644 index 0000000000..8630cb97fa --- /dev/null +++ b/models/migrations/v1_27/v341.go @@ -0,0 +1,80 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_27 + +import ( + "fmt" + "strings" + "time" + + "gitea.dev/models/db" + "gitea.dev/models/migrations/base" + + "xorm.io/xorm/schemas" +) + +// legacyDateTimeColumns are the persisted real datetime columns that old Gitea +// versions created as MSSQL DATETIME. Every other time value is stored as a +// unix timestamp integer, so these are the only columns affected. +var legacyDateTimeColumns = []struct { + bean any + column string +}{ + {new(externalLoginUserWithExpiresAt), "expires_at"}, + {new(lfsLockWithCreated), "created"}, +} + +type externalLoginUserWithExpiresAt struct { + ExpiresAt time.Time +} + +func (externalLoginUserWithExpiresAt) TableName() string { + return "external_login_user" +} + +type lfsLockWithCreated struct { + Created time.Time `xorm:"created"` +} + +func (lfsLockWithCreated) TableName() string { + return "lfs_lock" +} + +// FixLegacyMSSQLDateTimeColumns converts legacy locale-dependent DATETIME columns +// to DATETIME2. Databases created by old Gitea versions stored these columns as +// DATETIME, which fails to parse ISO datetime strings ('YYYY-MM-DD HH:MM:SS') +// when the MSSQL session language is not English, breaking external account +// linking and LFS lock creation. New installs already use DATETIME2, so only +// legacy MSSQL columns need converting. +func FixLegacyMSSQLDateTimeColumns(x db.EngineMigration) error { + if x.Dialect().URI().DBType != schemas.MSSQL { + return nil + } + + for _, c := range legacyDateTimeColumns { + table, err := x.TableInfo(c.bean) + if err != nil { + return err + } + + var dataType string + has, err := x.SQL("SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?", table.Name, c.column).Get(&dataType) + if err != nil { + return err + } + if !has || !strings.EqualFold(dataType, "datetime") { + continue + } + + column := table.GetColumn(c.column) + if column == nil { + return fmt.Errorf("column %s does not exist in table %s", c.column, table.Name) + } + if err := base.ModifyColumn(x, table.Name, column); err != nil { + return fmt.Errorf("modify %s.%s: %w", table.Name, c.column, err) + } + } + + return nil +} diff --git a/models/migrations/v1_27/v341_test.go b/models/migrations/v1_27/v341_test.go new file mode 100644 index 0000000000..b0fe6d24df --- /dev/null +++ b/models/migrations/v1_27/v341_test.go @@ -0,0 +1,80 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_27 + +import ( + "testing" + "time" + + "gitea.dev/models/db" + "gitea.dev/models/migrations/migrationtest" + "gitea.dev/modules/setting" + + "github.com/stretchr/testify/require" +) + +type externalLoginUserBeforeDateTimeMigration struct { + ExternalID string `xorm:"pk NOT NULL"` + LoginSourceID int64 `xorm:"pk NOT NULL"` + ExpiresAt time.Time // sync creates DATETIME2; downgraded to legacy DATETIME via raw SQL below +} + +func (externalLoginUserBeforeDateTimeMigration) TableName() string { + return "external_login_user" +} + +type lfsLockBeforeDateTimeMigration struct { + ID int64 `xorm:"pk autoincr"` + Created time.Time `xorm:"created"` +} + +func (lfsLockBeforeDateTimeMigration) TableName() string { + return "lfs_lock" +} + +func Test_FixLegacyMSSQLDateTimeColumns(t *testing.T) { + if !setting.Database.Type.IsMSSQL() { + t.Skip("Only MSSQL needs to convert the legacy locale-dependent DATETIME columns") + } + + x, deferrable := migrationtest.PrepareTestEnv(t, 0, + new(externalLoginUserBeforeDateTimeMigration), + new(lfsLockBeforeDateTimeMigration), + ) + defer deferrable() + + // Force the legacy DATETIME column type that old Gitea versions created. + _, err := x.Exec("ALTER TABLE [external_login_user] ALTER COLUMN [expires_at] DATETIME") + require.NoError(t, err) + _, err = x.Exec("ALTER TABLE [lfs_lock] ALTER COLUMN [created] DATETIME") + require.NoError(t, err) + require.Equal(t, "datetime", mssqlColumnType(t, x, "external_login_user", "expires_at")) + require.Equal(t, "datetime", mssqlColumnType(t, x, "lfs_lock", "created")) + + require.NoError(t, FixLegacyMSSQLDateTimeColumns(x)) + require.NoError(t, FixLegacyMSSQLDateTimeColumns(x)) // idempotent + + require.Equal(t, "datetime2", mssqlColumnType(t, x, "external_login_user", "expires_at")) + require.Equal(t, "datetime2", mssqlColumnType(t, x, "lfs_lock", "created")) + + // Inserting an ISO-formatted datetime must succeed even under a non-English + // locale, which is the failure the legacy DATETIME columns produced. The + // SET LANGUAGE and INSERT run in one Exec so they share a single connection. + _, err = x.Exec("SET LANGUAGE German; " + + "INSERT INTO [external_login_user] ([external_id], [login_source_id], [expires_at]) " + + "VALUES ('ext-id', 1, '2026-06-25 11:58:39')") + require.NoError(t, err) + _, err = x.Exec("SET LANGUAGE German; " + + "INSERT INTO [lfs_lock] ([created]) VALUES ('2026-06-25 11:58:39')") + require.NoError(t, err) +} + +func mssqlColumnType(t *testing.T, x db.EngineMigration, table, column string) string { + t.Helper() + var dataType string + has, err := x.SQL("SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?", table, column).Get(&dataType) + require.NoError(t, err) + require.True(t, has) + return dataType +} diff --git a/models/migrations/v1_27/v342.go b/models/migrations/v1_27/v342.go new file mode 100644 index 0000000000..7f57f552f6 --- /dev/null +++ b/models/migrations/v1_27/v342.go @@ -0,0 +1,42 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_27 + +import ( + "gitea.dev/models/db" + "gitea.dev/modules/timeutil" + + "xorm.io/xorm" +) + +func AddScopedWorkflowsSchema(x db.EngineMigration) error { + // Create the action_scoped_workflow_source table + type ScopedWorkflowConfig struct { + Required bool `json:"required"` + Patterns []string `json:"patterns"` + } + type ActionScopedWorkflowSource struct { + ID int64 `xorm:"pk autoincr"` + OwnerID int64 `xorm:"UNIQUE(owner_repo) NOT NULL DEFAULT 0"` + SourceRepoID int64 `xorm:"INDEX UNIQUE(owner_repo) NOT NULL DEFAULT 0"` + WorkflowConfigs map[string]*ScopedWorkflowConfig `xorm:"JSON TEXT 'workflow_configs'"` + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` + } + if err := x.Sync(new(ActionScopedWorkflowSource)); err != nil { + return err + } + + // Add the columns that record where a run's workflow content came from + type ActionRun struct { + WorkflowRepoID int64 `xorm:"NOT NULL DEFAULT 0"` + WorkflowCommitSHA string `xorm:"VARCHAR(64) NOT NULL DEFAULT ''"` + IsScopedRun bool `xorm:"NOT NULL DEFAULT false"` + } + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + IgnoreConstrains: true, + }, new(ActionRun)) + return err +} diff --git a/models/migrations/v1_9/v82.go b/models/migrations/v1_9/v82.go index 9e8b47e882..afcdc5c443 100644 --- a/models/migrations/v1_9/v82.go +++ b/models/migrations/v1_9/v82.go @@ -87,7 +87,7 @@ func FixReleaseSha1OnReleaseTable(ctx context.Context, x db.EngineMigration) err userCache[repo.OwnerID] = user } - gitRepo, err = gitrepo.OpenRepository(ctx, repo_model.StorageRepo(repo_model.RelativePath(user.Name, repo.Name))) + gitRepo, err = gitrepo.OpenRepository(repo_model.CodeRepoByName(user.Name, repo.Name)) if err != nil { return err } @@ -95,7 +95,7 @@ func FixReleaseSha1OnReleaseTable(ctx context.Context, x db.EngineMigration) err gitRepoCache[release.RepoID] = gitRepo } - release.Sha1, err = gitRepo.GetTagCommitID(release.TagName) + release.Sha1, err = gitRepo.GetTagCommitID(ctx, release.TagName) if err != nil && !git.IsErrNotExist(err) { return err } diff --git a/models/organization/org.go b/models/organization/org.go index 9a28aa3918..deb7d54b79 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -411,11 +411,8 @@ func GetOrgByName(ctx context.Context, name string) (*Organization, error) { if len(name) == 0 { return nil, ErrOrgNotExist{0, name} } - u := &Organization{ - LowerName: strings.ToLower(name), - Type: user_model.UserTypeOrganization, - } - has, err := db.GetEngine(ctx).Get(u) + + u, has, err := db.Get[Organization](ctx, builder.Eq{"lower_name": strings.ToLower(name), "`type`": user_model.UserTypeOrganization}) if err != nil { return nil, err } else if !has { diff --git a/models/organization/team.go b/models/organization/team.go index ea2f52a1ed..b419aa3225 100644 --- a/models/organization/team.go +++ b/models/organization/team.go @@ -6,6 +6,7 @@ package organization import ( "context" + "errors" "fmt" "strings" @@ -92,6 +93,15 @@ func (t *Team) IsPublic() bool { return t.Visibility.IsPublic() } func (t *Team) IsLimited() bool { return t.Visibility.IsLimited() } func (t *Team) IsPrivate() bool { return t.Visibility.IsPrivate() } +const ( + ghostTeamID = -1 + ghostTeamName = "(deleted team)" +) + +func newGhostTeam() *Team { + return &Team{ID: ghostTeamID, Name: ghostTeamName, LowerName: ghostTeamName} +} + // CanNonMemberReadMeta reports whether a non-member, non-owner doer may read // the team's metadata, based on the team's visibility tier and the parent org's // visibility. Privileged callers (site admins, org owners, team members) are @@ -107,8 +117,8 @@ func (t *Team) CanNonMemberReadMeta(ctx context.Context, org, doer *user_model.U } } -func NormalizeTeamVisibility(s string) structs.VisibleType { - if vt, ok := structs.VisibilityModes[s]; ok { +func NormalizeTeamVisibility[T ~string](v T) structs.VisibleType { + if vt, ok := structs.VisibilityModes[structs.VisibilityString(v)]; ok { return vt } return structs.VisibleTypePrivate @@ -270,6 +280,17 @@ func GetTeamByID(ctx context.Context, teamID int64) (*Team, error) { return t, nil } +func GetPossibleTeamByID(ctx context.Context, teamID int64) (int64, *Team, error) { + t, err := GetTeamByID(ctx, teamID) + if errors.Is(err, util.ErrNotExist) { + t = newGhostTeam() + return t.ID, t, nil + } else if err != nil { + return 0, nil, err + } + return t.ID, t, nil +} + // IncrTeamRepoNum increases the number of repos for the given team by 1 func IncrTeamRepoNum(ctx context.Context, teamID int64) error { _, err := db.GetEngine(ctx).Incr("num_repos").ID(teamID).Update(new(Team)) diff --git a/models/packages/package_property.go b/models/packages/package_property.go index 52815a9e43..6aa5c435a9 100644 --- a/models/packages/package_property.go +++ b/models/packages/package_property.go @@ -58,7 +58,7 @@ func GetProperties(ctx context.Context, refType PropertyType, refID int64) ([]*P // GetPropertiesByName gets all properties with a specific name func GetPropertiesByName(ctx context.Context, refType PropertyType, refID int64, name string) ([]*PackageProperty, error) { pps := make([]*PackageProperty, 0, 10) - return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).OrderBy("id").Find(&pps) + return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND `name` = ?", refType, refID, name).OrderBy("id").Find(&pps) } // UpdateProperty updates a property @@ -68,13 +68,12 @@ func UpdateProperty(ctx context.Context, pp *PackageProperty) error { } func InsertOrUpdateProperty(ctx context.Context, refType PropertyType, refID int64, name, value string) error { - pp := PackageProperty{RefType: refType, RefID: refID, Name: name} - ok, err := db.GetEngine(ctx).Get(&pp) + pp, ok, err := db.Get[PackageProperty](ctx, builder.Eq{"ref_type": refType, "ref_id": refID, "`name`": name}) if err != nil { return err } if ok { - _, err = db.GetEngine(ctx).Where("ref_type=? AND ref_id=? AND name=?", refType, refID, name).Cols("value").Update(&PackageProperty{Value: value}) + _, err = db.GetEngine(ctx).ID(pp.ID).Cols("value").Update(&PackageProperty{Value: value}) return err } _, err = InsertProperty(ctx, refType, refID, name, value) diff --git a/models/packages/package_version.go b/models/packages/package_version.go index 77b1da43de..d854545d0e 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -164,9 +164,7 @@ func DeleteVersionsByPackageID(ctx context.Context, packageID int64) error { // HasVersionFileReferences checks if there are associated files func HasVersionFileReferences(ctx context.Context, versionID int64) (bool, error) { - return db.GetEngine(ctx).Get(&PackageFile{ - VersionID: versionID, - }) + return db.Exist[PackageFile](ctx, builder.Eq{"version_id": versionID}) } // SearchValue describes a value to search diff --git a/models/perm/access/actions_repo_permission_test.go b/models/perm/access/actions_repo_permission_test.go index aee097cf93..9fdd8ae7f4 100644 --- a/models/perm/access/actions_repo_permission_test.go +++ b/models/perm/access/actions_repo_permission_test.go @@ -95,6 +95,40 @@ func TestGetActionsUserRepoPermission(t *testing.T) { assert.False(t, perm.CanRead(unit.TypeCode)) }) + t.Run("CollaborativeOwner_ForkPR_Denied", func(t *testing.T) { + // Target repo15 trusts repo2's owner as a collaborative owner. + repo15ActionsUnit := repo15.MustGetUnit(ctx, unit.TypeActions) + repo15ActionsUnit.ActionsConfig().AddCollaborativeOwner(owner2.ID) + require.NoError(t, repo_model.UpdateRepoUnitConfig(ctx, repo15ActionsUnit)) + + // Owner cross-repo policy does not allow repo15, so the only branch that + // could grant access is the collaborative-owner one. + require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, actions_model.OwnerActionsConfig{})) + + task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53}) + + // Non-fork task is legitimately granted code-read via collaborative owner. + task53.IsForkPullRequest = false + require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request")) + perm, err := GetActionsUserRepoPermission(ctx, repo15, actionsUser, task53.ID) + require.NoError(t, err) + assert.True(t, perm.CanRead(unit.TypeCode)) + + // Fork PR must NOT be able to read a third private repo through the + // collaborative-owner branch. + task53.IsForkPullRequest = true + require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request")) + perm, err = GetActionsUserRepoPermission(ctx, repo15, actionsUser, task53.ID) + require.NoError(t, err) + assert.False(t, perm.CanRead(unit.TypeCode)) + + // Restore state for subsequent subtests. + task53.IsForkPullRequest = false + require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request")) + repo15ActionsUnit.ActionsConfig().RemoveCollaborativeOwner(owner2.ID) + require.NoError(t, repo_model.UpdateRepoUnitConfig(ctx, repo15ActionsUnit)) + }) + t.Run("Inheritance_And_Clamping", func(t *testing.T) { task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53}) task53.IsForkPullRequest = false diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 2d53b0699f..69d54b9020 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -369,7 +369,9 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito // 2. The Actions Bot user has been explicitly granted access and repository is private // 3. The repository is public (handled by botPerm above) - if taskRepo.IsPrivate { + // Fork PRs are never allowed cross-repo access to other private repositories, + // matching the discriminator enforced by checkSameOwnerCrossRepoAccess above. + if taskRepo.IsPrivate && !task.IsForkPullRequest { actionsUnit := repo.MustGetUnit(ctx, unit.TypeActions) if actionsUnit.ActionsConfig().IsCollaborativeOwner(taskRepo.OwnerID) { return maxPerm, nil diff --git a/models/pull/review_state.go b/models/pull/review_state.go index 241a79d1df..13969d895a 100644 --- a/models/pull/review_state.go +++ b/models/pull/review_state.go @@ -11,6 +11,8 @@ import ( "gitea.dev/models/db" "gitea.dev/modules/log" "gitea.dev/modules/timeutil" + + "xorm.io/builder" ) // ViewedState stores for a file in which state it is currently viewed @@ -66,9 +68,14 @@ func (rs *ReviewState) GetViewedFileCount() int { // If the review didn't exist before in the database, it won't afterwards either. // The returned boolean shows whether the review exists in the database func GetReviewState(ctx context.Context, userID, pullID int64, commitSHA string) (*ReviewState, bool, error) { - review := &ReviewState{UserID: userID, PullID: pullID, CommitSHA: commitSHA} - has, err := db.GetEngine(ctx).Get(review) - return review, has, err + review, has, err := db.Get[ReviewState](ctx, builder.Eq{"user_id": userID, "pull_id": pullID, "commit_sha": commitSHA}) + if err != nil { + return nil, false, err + } + if review == nil { + review = &ReviewState{UserID: userID, PullID: pullID, CommitSHA: commitSHA} + } + return review, has, nil } // UpdateReviewState updates the given review inside the database, regardless of whether it existed before or not diff --git a/models/renderhelper/commit_checker.go b/models/renderhelper/commit_checker.go index faf2d94eb0..1a5eeb31a5 100644 --- a/models/renderhelper/commit_checker.go +++ b/models/renderhelper/commit_checker.go @@ -7,22 +7,23 @@ import ( "context" "io" + repo_model "gitea.dev/models/repo" "gitea.dev/modules/git" "gitea.dev/modules/gitrepo" "gitea.dev/modules/log" ) type commitChecker struct { - ctx context.Context - commitCache map[string]bool - gitRepoFacade gitrepo.Repository + ctx context.Context + commitCache map[string]bool + repo *repo_model.Repository gitRepo *git.Repository gitRepoCloser io.Closer } -func newCommitChecker(ctx context.Context, gitRepo gitrepo.Repository) *commitChecker { - return &commitChecker{ctx: ctx, commitCache: make(map[string]bool), gitRepoFacade: gitRepo} +func newCommitChecker(ctx context.Context, repo *repo_model.Repository) *commitChecker { + return &commitChecker{ctx: ctx, commitCache: make(map[string]bool), repo: repo} } func (c *commitChecker) Close() error { @@ -39,15 +40,15 @@ func (c *commitChecker) IsCommitIDExisting(commitID string) bool { } if c.gitRepo == nil { - r, closer, err := gitrepo.RepositoryFromContextOrOpen(c.ctx, c.gitRepoFacade) + r, closer, err := gitrepo.RepositoryFromContextOrOpen(c.ctx, c.repo) if err != nil { - log.Error("unable to open repository: %s Error: %v", gitrepo.RepoGitURL(c.gitRepoFacade), err) + log.Error("Unable to open repository: %s, error: %v", c.repo.FullName(), err) return false } c.gitRepo, c.gitRepoCloser = r, closer } - exist = c.gitRepo.IsReferenceExist(commitID) // Don't use IsObjectExist since it doesn't support short hashes with gogit edition. + exist = c.gitRepo.IsReferenceExist(c.ctx, commitID) // Don't use IsObjectExist since it doesn't support short hashes with gogit edition. c.commitCache[commitID] = exist return exist } diff --git a/models/repo/archiver.go b/models/repo/archiver.go index 751fbf4b9c..328b6c0981 100644 --- a/models/repo/archiver.go +++ b/models/repo/archiver.go @@ -80,7 +80,7 @@ func (archiver *RepoArchiver) RelativePath() string { return fmt.Sprintf("%d/%s/%s.%s", archiver.RepoID, archiver.CommitID[:2], archiver.CommitID, archiver.Type.String()) } -// repoArchiverForRelativePath takes a relativePath created from (archiver *RepoArchiver) RelativePath() and creates a shell repoArchiver struct representing it +// repoArchiverForRelativePath takes a relativePath created from RepoArchiver.RelativePath() and creates a shell repoArchiver struct representing it func repoArchiverForRelativePath(relativePath string) (*RepoArchiver, error) { parts := strings.SplitN(relativePath, "/", 3) if len(parts) != 3 { diff --git a/models/repo/attachment.go b/models/repo/attachment.go index b7136b657b..cb74f310d4 100644 --- a/models/repo/attachment.go +++ b/models/repo/attachment.go @@ -17,6 +17,8 @@ import ( "gitea.dev/modules/storage" "gitea.dev/modules/timeutil" "gitea.dev/modules/util" + + "xorm.io/builder" ) // Attachment represent a attachment of issue/comment/release. @@ -156,8 +158,7 @@ func GetAttachmentsByCommentID(ctx context.Context, commentID int64) ([]*Attachm // GetAttachmentByReleaseIDFileName returns attachment by given releaseId and fileName. func GetAttachmentByReleaseIDFileName(ctx context.Context, releaseID int64, fileName string) (*Attachment, error) { - attach := &Attachment{ReleaseID: releaseID, Name: fileName} - has, err := db.GetEngine(ctx).Get(attach) + attach, has, err := db.Get[Attachment](ctx, builder.Eq{"release_id": releaseID, "`name`": fileName}) if err != nil { return nil, err } else if !has { diff --git a/models/repo/collaboration.go b/models/repo/collaboration.go index 5748265f96..9173ec733e 100644 --- a/models/repo/collaboration.go +++ b/models/repo/collaboration.go @@ -102,20 +102,13 @@ func GetCollaborators(ctx context.Context, opts *FindCollaborationOptions) ([]*C // GetCollaboration get collaboration for a repository id with a user id func GetCollaboration(ctx context.Context, repoID, uid int64) (*Collaboration, error) { - collaboration := &Collaboration{ - RepoID: repoID, - UserID: uid, - } - has, err := db.GetEngine(ctx).Get(collaboration) - if !has { - collaboration = nil - } + collaboration, _, err := db.Get[Collaboration](ctx, builder.Eq{"repo_id": repoID, "user_id": uid}) return collaboration, err } // IsCollaborator check if a user is a collaborator of a repository func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) { - return db.GetEngine(ctx).Get(&Collaboration{RepoID: repoID, UserID: userID}) + return db.Exist[Collaboration](ctx, builder.Eq{"repo_id": repoID, "user_id": userID}) } // ChangeCollaborationAccessMode sets new access mode for the collaboration. @@ -126,13 +119,7 @@ func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid in } return db.WithTx(ctx, func(ctx context.Context) error { - e := db.GetEngine(ctx) - - collaboration := &Collaboration{ - RepoID: repo.ID, - UserID: uid, - } - has, err := e.Get(collaboration) + collaboration, has, err := db.Get[Collaboration](ctx, builder.Eq{"repo_id": repo.ID, "user_id": uid}) if err != nil { return fmt.Errorf("get collaboration: %w", err) } else if !has { @@ -144,12 +131,12 @@ func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid in } collaboration.Mode = mode - if _, err = e. + if _, err = db.GetEngine(ctx). ID(collaboration.ID). Cols("mode"). Update(collaboration); err != nil { return fmt.Errorf("update collaboration: %w", err) - } else if _, err = e.Exec("UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil { + } else if _, err = db.Exec(ctx, "UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil { return fmt.Errorf("update access table: %w", err) } @@ -174,5 +161,5 @@ func IsOwnerMemberCollaborator(ctx context.Context, repo *Repository, userID int return true, nil } - return db.GetEngine(ctx).Get(&Collaboration{RepoID: repo.ID, UserID: userID}) + return db.Exist[Collaboration](ctx, builder.Eq{"repo_id": repo.ID, "user_id": userID}) } diff --git a/models/repo/mirror.go b/models/repo/mirror.go index 8bfff01a02..1985b49963 100644 --- a/models/repo/mirror.go +++ b/models/repo/mirror.go @@ -12,6 +12,8 @@ import ( "gitea.dev/modules/log" "gitea.dev/modules/timeutil" "gitea.dev/modules/util" + + "xorm.io/builder" ) // ErrMirrorNotExist mirror does not exist error @@ -76,8 +78,7 @@ func (m *Mirror) ScheduleNextUpdate() { // GetMirrorByRepoID returns mirror information of a repository. func GetMirrorByRepoID(ctx context.Context, repoID int64) (*Mirror, error) { - m := &Mirror{RepoID: repoID} - has, err := db.GetEngine(ctx).Get(m) + m, has, err := db.Get[Mirror](ctx, builder.Eq{"repo_id": repoID}) if err != nil { return nil, err } else if !has { diff --git a/models/repo/org_repo.go b/models/repo/org_repo.go index ef4d9a4a62..5bf0a3e7d4 100644 --- a/models/repo/org_repo.go +++ b/models/repo/org_repo.go @@ -31,18 +31,28 @@ func GetOrgRepositoryIDs(ctx context.Context, orgID int64) (repoIDs []int64, _ e type SearchTeamRepoOptions struct { db.ListOptions TeamID int64 + // PublicOnly restricts the result (and count) to non-private repositories. + PublicOnly bool +} + +func (opts *SearchTeamRepoOptions) toCond() builder.Cond { + cond := builder.NewCond() + if opts.TeamID > 0 { + cond = cond.And(builder.In("id", + builder.Select("repo_id"). + From("team_repo"). + Where(builder.Eq{"team_id": opts.TeamID}), + )) + } + if opts.PublicOnly { + cond = cond.And(builder.Eq{"is_private": false}) + } + return cond } // 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 { - sess = sess.In("id", - builder.Select("repo_id"). - From("team_repo"). - Where(builder.Eq{"team_id": opts.TeamID}), - ) - } + sess := db.GetEngine(ctx).Where(opts.toCond()) if opts.PageSize > 0 { sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) } @@ -51,6 +61,11 @@ func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (Repo Find(&repos) } +// CountTeamRepositories returns the number of repositories in team of organization matching opts. +func CountTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (int64, error) { + return db.GetEngine(ctx).Where(opts.toCond()).Count(new(Repository)) +} + // AccessibleReposEnvironment operations involving the repositories that are // accessible to a particular user type AccessibleReposEnvironment interface { diff --git a/models/repo/redirect.go b/models/repo/redirect.go index 405feb5782..c51c5d69b0 100644 --- a/models/repo/redirect.go +++ b/models/repo/redirect.go @@ -10,6 +10,8 @@ import ( "gitea.dev/models/db" "gitea.dev/modules/util" + + "xorm.io/builder" ) // ErrRedirectNotExist represents a "RedirectNotExist" kind of error. @@ -52,8 +54,8 @@ func init() { // LookupRedirect look up if a repository has a redirect name func LookupRedirect(ctx context.Context, ownerID int64, repoName string) (int64, error) { repoName = strings.ToLower(repoName) - redirect := &Redirect{OwnerID: ownerID, LowerName: repoName} - if has, err := db.GetEngine(ctx).Get(redirect); err != nil { + redirect, has, err := db.Get[Redirect](ctx, builder.Eq{"owner_id": ownerID, "lower_name": repoName}) + if err != nil { return 0, err } else if !has { return 0, ErrRedirectNotExist{OwnerID: ownerID, RepoName: repoName} diff --git a/models/repo/release.go b/models/repo/release.go index 2c3ee18757..b4f5fa902b 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -216,8 +216,7 @@ func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs // GetRelease returns release by given ID. func GetRelease(ctx context.Context, repoID int64, tagName string) (*Release, error) { - rel := &Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)} - has, err := db.GetEngine(ctx).Get(rel) + rel, has, err := db.Get[Release](ctx, builder.Eq{"repo_id": repoID, "lower_tag_name": strings.ToLower(tagName)}) if err != nil { return nil, err } else if !has { diff --git a/models/repo/repo.go b/models/repo/repo.go index e56603fc81..2161ae564d 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -226,22 +226,6 @@ func init() { db.RegisterModel(new(Repository)) } -func RelativePath(ownerName, repoName string) string { - return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".git" -} - -// RelativePath should be an unix style path like username/reponame.git -func (repo *Repository) RelativePath() string { - return RelativePath(repo.OwnerName, repo.Name) -} - -type StorageRepo string - -// RelativePath should be an unix style path like username/reponame.git -func (sr StorageRepo) RelativePath() string { - return string(sr) -} - // SanitizedOriginalURL returns a sanitized OriginalURL func (repo *Repository) SanitizedOriginalURL() string { if repo.OriginalURL == "" { @@ -647,6 +631,10 @@ func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML { // CloneLink represents different types of clone URLs of repository. type CloneLink struct { + IsWikiRepo bool + SupportSSH bool + SupportHTTPS bool + SSH string HTTPS string Tea string @@ -698,9 +686,12 @@ func ComposeTeaCloneCommand(ctx context.Context, owner, repo string) string { func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string) *CloneLink { return &CloneLink{ - SSH: ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName), - HTTPS: ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName), - Tea: ComposeTeaCloneCommand(ctx, repo.OwnerName, repoPathName), + IsWikiRepo: strings.HasSuffix(repoPathName, ".wiki"), + SupportHTTPS: !setting.Repository.DisableHTTPGit, + SupportSSH: !setting.SSH.Disabled && (doer != nil || setting.SSH.ExposeAnonymous), + SSH: ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName), + HTTPS: ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName), + Tea: ComposeTeaCloneCommand(ctx, repo.OwnerName, repoPathName), } } @@ -849,9 +840,9 @@ func GetRepositoriesMapByIDs(ctx context.Context, ids []int64) (map[int64]*Repos } func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) { - return db.GetEngine(ctx).Get(&Repository{ - OwnerID: u.ID, - LowerName: strings.ToLower(repoName), + return db.Exist[Repository](ctx, builder.Eq{ + "owner_id": u.ID, + "lower_name": strings.ToLower(repoName), }) } diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 57f9e78833..95eca36235 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -310,11 +310,17 @@ func userOrgTeamRepoBuilder(userID int64) *builder.Builder { } // userOrgTeamUnitRepoBuilder returns repo ids where user's teams can access the special unit. +// A team grants the unit either through an explicit team_unit row (access_mode > none) or by being an +// admin/owner team (team.authorize >= admin), which grants every unit regardless of team_unit rows — +// mirroring the HasAdminAccess() short-circuit in access.GetIndividualUserRepoPermission. func userOrgTeamUnitRepoBuilder(userID int64, unitType unit.Type) *builder.Builder { return userOrgTeamRepoBuilder(userID). - Join("INNER", "team_unit", "`team_unit`.team_id = `team_repo`.team_id"). - Where(builder.Eq{"`team_unit`.`type`": unitType}). - And(builder.Gt{"`team_unit`.`access_mode`": int(perm.AccessModeNone)}) + Join("INNER", "team", "`team`.id = `team_repo`.team_id"). + Join("LEFT", "team_unit", builder.Expr("`team_unit`.team_id = `team_repo`.team_id AND `team_unit`.`type` = ?", unitType)). + Where(builder.Or( + builder.Gte{"`team`.authorize": int(perm.AccessModeAdmin)}, + builder.Gt{"`team_unit`.`access_mode`": int(perm.AccessModeNone)}, + )) } // userOrgTeamUnitRepoCond returns a condition to select repo ids where user's teams can access the special unit. @@ -326,7 +332,7 @@ func userOrgTeamUnitRepoCond(idStr string, userID int64, unitType unit.Type) bui func UserOrgUnitRepoCond(idStr string, userID, orgID int64, unitType unit.Type) builder.Cond { return builder.In(idStr, userOrgTeamUnitRepoBuilder(userID, unitType). - And(builder.Eq{"`team_unit`.org_id": orgID}), + And(builder.Eq{"`team`.org_id": orgID}), ) } @@ -755,6 +761,40 @@ func FindUserCodeAccessibleOwnerRepoIDs(ctx context.Context, ownerID int64, user )) } +// PublicRepoUnderPublicOwnerCond restricts to public repos whose owner is publicly visible: the +// "genuinely public" set a public-only token or an anonymous caller may see (a public repo under a +// limited/private owner is not publicly reachable and must be excluded). +func PublicRepoUnderPublicOwnerCond() builder.Cond { + return builder.And( + builder.Eq{"`repository`.is_private": false}, + builder.In("`repository`.owner_id", builder.Select("id").From("`user`").Where(builder.Eq{"visibility": structs.VisibleTypePublic})), + ) +} + +// UserActionsAccessibleOwnerRepoCond selects the repos owned by ownerID whose Actions `user` may read. +// It is used to list an org/user's Actions runs and jobs (see the callers in routers/api/v1/shared). +// - owner_id = ownerID: only that owner's repos. +// - AccessibleRepositoryCondition(user, TypeActions): only repos whose Actions the user can read +// (admin/owner teams are handled inside it; a site admin is not, callers must skip the filter for one). +// - publicOnly (a public-only token): additionally limit to public repos under a public owner. +func UserActionsAccessibleOwnerRepoCond(ownerID int64, user *user_model.User, publicOnly bool) builder.Cond { + cond := builder.NewCond().And( + builder.Eq{"`repository`.owner_id": ownerID}, + AccessibleRepositoryCondition(user, unit.TypeActions), + ) + if publicOnly { + cond = cond.And(PublicRepoUnderPublicOwnerCond()) + } + return cond +} + +// FindUserActionsAccessibleOwnerRepoIDsSubQuery returns a subquery selecting the repository IDs the user +// can see for the given owner. Callers embed it in an `IN (...)` condition so that a large owner does not +// materialize every repo ID into the SQL statement, which could exceed database parameter limits. +func FindUserActionsAccessibleOwnerRepoIDsSubQuery(ownerID int64, user *user_model.User, publicOnly bool) *builder.Builder { + return builder.Select("id").From("repository").Where(UserActionsAccessibleOwnerRepoCond(ownerID, user, publicOnly)) +} + // GetUserRepositories returns a list of repositories of given user. func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) { if len(opts.OrderBy) == 0 { diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go index 78ede84150..e3db58af23 100644 --- a/models/repo/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -9,6 +9,7 @@ import ( "gitea.dev/models/db" repo_model "gitea.dev/models/repo" + "gitea.dev/models/unit" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" "gitea.dev/modules/optional" @@ -466,3 +467,50 @@ func TestSearchRepositoryByTopicName(t *testing.T) { }) } } + +func TestFindUserActionsAccessibleOwnerRepoIDs(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + // user2 is on org3's owner team, so it can access org3's private repo3 (which has the actions unit) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // org3 is a public org owning repo3 (private) and repo32 (public), both with the actions unit + const orgID = 3 + + all, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, user, false)) + require.NoError(t, err) + assert.Contains(t, all, int64(3), "without public-only the private repo's actions are listed") + + publicOnly, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, user, true)) + require.NoError(t, err) + assert.NotContains(t, publicOnly, int64(3), "a public-only token must not list a private repo's actions") + assert.Contains(t, publicOnly, int64(32), "a public repo under a public owner stays listed") +} + +// TestUserOrgUnitRepoCondTeamAuthorize pins the team.authorize behavior of userOrgTeamUnitRepoBuilder +// (exercised through UserOrgUnitRepoCond): an admin/owner team grants every unit even without an explicit +// team_unit row, while a non-admin team only grants a unit it has an explicit row for. This guards both +// directions — hiding repos from admin-team members, and over-broadening a plain team's access. +func TestUserOrgUnitRepoCondTeamAuthorize(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + accessibleRepoIDs := func(userID, orgID int64, unitType unit.Type) []int64 { + ids, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), + repo_model.UserOrgUnitRepoCond("`repository`.id", userID, orgID, unitType)) + require.NoError(t, err) + return ids + } + + // Case A: user18 is only on org17's owner team (team5, authorize=owner), linked to the private repo24 + // but with no Actions team_unit row. The owner authorize must still grant it, mirroring the runtime + // HasAdminAccess() short-circuit in access.GetIndividualUserRepoPermission. + assert.Contains(t, accessibleRepoIDs(18, 17, unit.TypeActions), int64(24), + "an owner team grants a unit it has no explicit team_unit row for") + + // Cases B and C share one subject so the team_unit row is the only difference: user4 is only on org3's + // write team (team2, authorize=write, non-admin), linked to the private repo3. team2 has an explicit + // Projects row but none for Actions. + assert.Contains(t, accessibleRepoIDs(4, 3, unit.TypeProjects), int64(3), + "a non-admin team grants a unit it has an explicit team_unit row for") + assert.NotContains(t, accessibleRepoIDs(4, 3, unit.TypeActions), int64(3), + "a non-admin team must NOT grant a unit it has no team_unit row for") +} diff --git a/models/repo/repo_location.go b/models/repo/repo_location.go new file mode 100644 index 0000000000..64ded367d8 --- /dev/null +++ b/models/repo/repo_location.go @@ -0,0 +1,56 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "strconv" + "strings" + + "gitea.dev/modules/git/gitcmd" +) + +func repoCodeGitRepoRelativePath(ownerName, repoName string) string { + return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".git" +} + +func repoWikiGitRepoRelativePath(ownerName, repoName string) string { + return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".wiki.git" +} + +// CodeRepoByName returns an unmanaged repository facade for the code repository of the given owner and repository name. +// Usually it is used for migration fixes or repository adoption/creation/rename/transfer. +func CodeRepoByName(ownerName, repoName string) gitcmd.RepositoryFacade { + return gitcmd.RepositoryUnmanaged(repoCodeGitRepoRelativePath(ownerName, repoName)) +} + +func WikiRepoByName(ownerName, repoName string) gitcmd.RepositoryFacade { + return gitcmd.RepositoryUnmanaged(repoWikiGitRepoRelativePath(ownerName, repoName)) +} + +func repoCodeGitRepoManagedID(repoID int64) string { + return "repo-" + strconv.FormatInt(repoID, 10) +} + +func (repo *Repository) CodeStorageRepo() gitcmd.RepositoryFacade { + id := repoCodeGitRepoManagedID(repo.ID) + repoPath := repoCodeGitRepoRelativePath(repo.OwnerName, repo.Name) + return gitcmd.RepositoryManaged(id, repoPath) +} + +func (repo *Repository) GitRepoLocation() string { + // TODO: use CodeGitRepo instead of this one + return repoCodeGitRepoRelativePath(repo.OwnerName, repo.Name) +} + +func (repo *Repository) GitRepoManagedID() string { + // TODO: use CodeGitRepo instead of this one + return repoCodeGitRepoManagedID(repo.ID) +} + +func (repo *Repository) WikiStorageRepo() gitcmd.RepositoryFacade { + // The wiki repository should have the same object format as the code repository. TODO: old comment, REALLY? Why? + id := "repo-wiki-" + strconv.FormatInt(repo.ID, 10) + repoPath := repoWikiGitRepoRelativePath(repo.OwnerName, repo.Name) + return gitcmd.RepositoryManaged(id, repoPath) +} diff --git a/models/repo/repo_location_test.go b/models/repo/repo_location_test.go new file mode 100644 index 0000000000..349db95567 --- /dev/null +++ b/models/repo/repo_location_test.go @@ -0,0 +1,27 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo_test + +import ( + "testing" + + repo_model "gitea.dev/models/repo" + "gitea.dev/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestRepository_GitRepo(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + assert.Equal(t, "user2/repo1.git", repo_model.CodeRepoByName(repo.OwnerName, repo.Name).GitRepoLocation()) + assert.Equal(t, "user2/repo1.git", repo.CodeStorageRepo().GitRepoLocation()) + assert.Equal(t, "repo-1", repo.CodeStorageRepo().GitRepoManagedID()) + + assert.Equal(t, "user2/repo1.wiki.git", repo_model.WikiRepoByName(repo.OwnerName, repo.Name).GitRepoLocation()) + assert.Equal(t, "user2/repo1.wiki.git", repo.WikiStorageRepo().GitRepoLocation()) + assert.Equal(t, "repo-wiki-1", repo.WikiStorageRepo().GitRepoManagedID()) +} diff --git a/models/repo/repo_unit_actions.go b/models/repo/repo_unit_actions.go index da162027b4..3bba5de12a 100644 --- a/models/repo/repo_unit_actions.go +++ b/models/repo/repo_unit_actions.go @@ -70,6 +70,8 @@ func MakeRestrictedPermissions() ActionsTokenPermissions { type ActionsConfig struct { DisabledWorkflows []string + // DisabledScopedWorkflows maps a scoped workflow's source repository ID to the entry names opted out of in this repository. + DisabledScopedWorkflows map[int64][]string // CollaborativeOwnerIDs is a list of owner IDs used to share actions from private repos. // Only workflows from the private repos whose owners are in CollaborativeOwnerIDs can access the current repo's actions. CollaborativeOwnerIDs []int64 @@ -98,6 +100,29 @@ func (cfg *ActionsConfig) DisableWorkflow(file string) { cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file) } +func (cfg *ActionsConfig) IsScopedWorkflowDisabled(sourceRepoID int64, workflowID string) bool { + return slices.Contains(cfg.DisabledScopedWorkflows[sourceRepoID], workflowID) +} + +func (cfg *ActionsConfig) DisableScopedWorkflow(sourceRepoID int64, workflowID string) { + if slices.Contains(cfg.DisabledScopedWorkflows[sourceRepoID], workflowID) { + return + } + if cfg.DisabledScopedWorkflows == nil { + cfg.DisabledScopedWorkflows = make(map[int64][]string) + } + cfg.DisabledScopedWorkflows[sourceRepoID] = append(cfg.DisabledScopedWorkflows[sourceRepoID], workflowID) +} + +func (cfg *ActionsConfig) EnableScopedWorkflow(sourceRepoID int64, workflowID string) { + workflowIDs := util.SliceRemoveAll(cfg.DisabledScopedWorkflows[sourceRepoID], workflowID) + if len(workflowIDs) == 0 { + delete(cfg.DisabledScopedWorkflows, sourceRepoID) + return + } + cfg.DisabledScopedWorkflows[sourceRepoID] = workflowIDs +} + func (cfg *ActionsConfig) AddCollaborativeOwner(ownerID int64) { if !slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) { cfg.CollaborativeOwnerIDs = append(cfg.CollaborativeOwnerIDs, ownerID) diff --git a/models/repo/repo_unit_actions_test.go b/models/repo/repo_unit_actions_test.go new file mode 100644 index 0000000000..552c090fd9 --- /dev/null +++ b/models/repo/repo_unit_actions_test.go @@ -0,0 +1,51 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestActionsConfig_ScopedWorkflowOptOut(t *testing.T) { + cfg := &ActionsConfig{} + + assert.False(t, cfg.IsScopedWorkflowDisabled(100, "ci.yml")) + + cfg.DisableScopedWorkflow(100, "ci.yml") + assert.True(t, cfg.IsScopedWorkflowDisabled(100, "ci.yml")) + + // idempotent + cfg.DisableScopedWorkflow(100, "ci.yml") + assert.Len(t, cfg.DisabledScopedWorkflows, 1) + + // keyed by source repo: the same filename from a different source repo is independent + assert.False(t, cfg.IsScopedWorkflowDisabled(200, "ci.yml")) + + // must not collide with the repo-level DisabledWorkflows list (bare filename) + assert.False(t, cfg.IsWorkflowDisabled("ci.yml")) + cfg.DisableWorkflow("ci.yml") + assert.True(t, cfg.IsWorkflowDisabled("ci.yml")) + assert.True(t, cfg.IsScopedWorkflowDisabled(100, "ci.yml"), "repo-level disable must not touch the scoped entry") + + cfg.EnableScopedWorkflow(100, "ci.yml") + assert.False(t, cfg.IsScopedWorkflowDisabled(100, "ci.yml")) + assert.True(t, cfg.IsWorkflowDisabled("ci.yml"), "enabling the scoped entry must not touch the repo-level disable") +} + +func TestActionsConfig_ScopedWorkflowSerialization(t *testing.T) { + cfg := &ActionsConfig{} + cfg.DisableScopedWorkflow(100, "ci.yml") + cfg.DisableWorkflow("repo.yml") + + bs, err := cfg.ToDB() + require.NoError(t, err) + + got := &ActionsConfig{} + require.NoError(t, got.FromDB(bs)) + assert.True(t, got.IsScopedWorkflowDisabled(100, "ci.yml")) + assert.True(t, got.IsWorkflowDisabled("repo.yml")) +} diff --git a/models/repo/star.go b/models/repo/star.go index c3f1203fec..cecaecfdea 100644 --- a/models/repo/star.go +++ b/models/repo/star.go @@ -9,6 +9,8 @@ import ( "gitea.dev/models/db" user_model "gitea.dev/models/user" "gitea.dev/modules/timeutil" + + "xorm.io/builder" ) // Star represents a starred repo by a user. @@ -68,7 +70,7 @@ func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star // IsStaring checks if user has starred given repository. func IsStaring(ctx context.Context, userID, repoID int64) bool { - has, _ := db.GetEngine(ctx).Get(&Star{UID: userID, RepoID: repoID}) + has, _ := db.Exist[Star](ctx, builder.Eq{"uid": userID, "repo_id": repoID}) return has } diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index 94425c0a1d..741626eddd 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -22,6 +22,9 @@ type StarredReposOptions struct { StarrerID int64 RepoOwnerID int64 IncludePrivate bool + // Actor is the user the private repositories are gated on: a private repo is only + // returned when Actor still has access to it, even if it was starred while access was granted. + Actor *user_model.User } func (opts *StarredReposOptions) ApplyPublicOnly(publicOnly bool) { @@ -39,10 +42,12 @@ func (opts *StarredReposOptions) ToConds() builder.Cond { "repository.owner_id": opts.RepoOwnerID, }) } - if !opts.IncludePrivate { - cond = cond.And(builder.Eq{ - "repository.is_private": false, - }) + if opts.IncludePrivate { + // only include private repos the actor can still access, so metadata does not leak after access revocation + cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid)) + } else { + // a public repo under a limited/private owner is not publicly reachable, so exclude it too + cond = cond.And(PublicRepoUnderPublicOwnerCond()) } return cond } @@ -66,6 +71,9 @@ type WatchedReposOptions struct { WatcherID int64 RepoOwnerID int64 IncludePrivate bool + // Actor is the user the private repositories are gated on: a private repo is only + // returned when Actor still has access to it, even if it was watched while access was granted. + Actor *user_model.User } func (opts *WatchedReposOptions) ApplyPublicOnly(publicOnly bool) { @@ -83,10 +91,12 @@ func (opts *WatchedReposOptions) ToConds() builder.Cond { "repository.owner_id": opts.RepoOwnerID, }) } - if !opts.IncludePrivate { - cond = cond.And(builder.Eq{ - "repository.is_private": false, - }) + if opts.IncludePrivate { + // only include private repos the actor can still access, so metadata does not leak after access revocation + cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid)) + } else { + // a public repo under a limited/private owner is not publicly reachable, so exclude it too + cond = cond.And(PublicRepoUnderPublicOwnerCond()) } return cond.And(builder.Neq{ "watch.mode": WatchModeDont, diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go index d21c77aa0a..264fa003ca 100644 --- a/models/repo/user_repo_test.go +++ b/models/repo/user_repo_test.go @@ -84,3 +84,40 @@ func testUserRepoGetIssuePostersWithSearch(t *testing.T) { require.Len(t, users, 1) assert.Equal(t, "user2", users[0].Name) } + +func TestStarredWatchedReposExcludeNonPublicOwners(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + const viewerID = 2 + // repo1: public repo under a public owner; repo38: public repo under a limited org (not publicly reachable) + const publicOwnerRepo, limitedOwnerRepo = 1, 38 + + require.NoError(t, db.Insert(t.Context(), &repo_model.Star{UID: viewerID, RepoID: publicOwnerRepo})) + require.NoError(t, db.Insert(t.Context(), &repo_model.Star{UID: viewerID, RepoID: limitedOwnerRepo})) + require.NoError(t, db.Insert(t.Context(), &repo_model.Watch{UserID: viewerID, RepoID: publicOwnerRepo, Mode: repo_model.WatchModeNormal})) + require.NoError(t, db.Insert(t.Context(), &repo_model.Watch{UserID: viewerID, RepoID: limitedOwnerRepo, Mode: repo_model.WatchModeNormal})) + + listOpts := db.ListOptions{Page: 1, PageSize: 50} + + starred, err := repo_model.GetStarredRepos(t.Context(), &repo_model.StarredReposOptions{ + ListOptions: listOpts, StarrerID: viewerID, IncludePrivate: false, + }) + require.NoError(t, err) + assert.NotContains(t, repoIDs(starred), int64(limitedOwnerRepo), "a public repo under a limited owner must be hidden from a public star listing") + assert.Contains(t, repoIDs(starred), int64(publicOwnerRepo), "a public repo under a public owner stays visible") + + watched, _, err := repo_model.GetWatchedRepos(t.Context(), &repo_model.WatchedReposOptions{ + ListOptions: listOpts, WatcherID: viewerID, IncludePrivate: false, + }) + require.NoError(t, err) + assert.NotContains(t, repoIDs(watched), int64(limitedOwnerRepo), "a public repo under a limited owner must be hidden from a public watch listing") + assert.Contains(t, repoIDs(watched), int64(publicOwnerRepo), "a public repo under a public owner stays visible") +} + +func repoIDs(repos []*repo_model.Repository) []int64 { + ids := make([]int64, len(repos)) + for i, r := range repos { + ids[i] = r.ID + } + return ids +} diff --git a/models/repo/watch.go b/models/repo/watch.go index 3d349dde3e..7b7c331c2b 100644 --- a/models/repo/watch.go +++ b/models/repo/watch.go @@ -10,6 +10,8 @@ import ( user_model "gitea.dev/models/user" "gitea.dev/modules/setting" "gitea.dev/modules/timeutil" + + "xorm.io/builder" ) // WatchMode specifies what kind of watch the user has on a repository @@ -41,12 +43,14 @@ func init() { } // GetWatch gets what kind of subscription a user has on a given repository; returns dummy record if none found -func GetWatch(ctx context.Context, userID, repoID int64) (Watch, error) { - watch := Watch{UserID: userID, RepoID: repoID} - has, err := db.GetEngine(ctx).Get(&watch) +func GetWatch(ctx context.Context, userID, repoID int64) (*Watch, error) { + watch, has, err := db.Get[Watch](ctx, builder.Eq{"user_id": userID, "repo_id": repoID}) if err != nil { return watch, err } + if watch == nil { + watch = &Watch{UserID: userID, RepoID: repoID} + } if !has { watch.Mode = WatchModeNone } @@ -64,7 +68,7 @@ func IsWatching(ctx context.Context, userID, repoID int64) bool { return err == nil && IsWatchMode(watch.Mode) } -func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error) { +func watchRepoMode(ctx context.Context, watch *Watch, mode WatchMode) (err error) { if watch.Mode == mode { return nil } diff --git a/models/repo/wiki.go b/models/repo/wiki.go index cbc1229c9f..5af8350bbb 100644 --- a/models/repo/wiki.go +++ b/models/repo/wiki.go @@ -7,7 +7,6 @@ package repo import ( "context" "fmt" - "strings" user_model "gitea.dev/models/user" "gitea.dev/modules/util" @@ -74,13 +73,3 @@ func (err ErrWikiInvalidFileName) Unwrap() error { func (repo *Repository) WikiCloneLink(ctx context.Context, doer *user_model.User) *CloneLink { return repo.cloneLink(ctx, doer, repo.Name+".wiki") } - -func RelativeWikiPath(ownerName, repoName string) string { - return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".wiki.git" -} - -// WikiStorageRepo returns the storage repo for the wiki -// The wiki repository should have the same object format as the code repository -func (repo *Repository) WikiStorageRepo() StorageRepo { - return StorageRepo(RelativeWikiPath(repo.OwnerName, repo.Name)) -} diff --git a/models/repo/wiki_test.go b/models/repo/wiki_test.go index 62a881f6f8..1da976b6ac 100644 --- a/models/repo/wiki_test.go +++ b/models/repo/wiki_test.go @@ -20,11 +20,3 @@ func TestRepository_WikiCloneLink(t *testing.T) { assert.Equal(t, "ssh://sshuser@try.gitea.io:3000/user2/repo1.wiki.git", cloneLink.SSH) assert.Equal(t, "https://try.gitea.io/user2/repo1.wiki.git", cloneLink.HTTPS) } - -func TestRepository_RelativeWikiPath(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - assert.Equal(t, "user2/repo1.wiki.git", repo_model.RelativeWikiPath(repo.OwnerName, repo.Name)) - assert.Equal(t, "user2/repo1.wiki.git", repo.WikiStorageRepo().RelativePath()) -} diff --git a/models/system/appstate.go b/models/system/appstate.go index 34a136e3bd..c7203a52ec 100644 --- a/models/system/appstate.go +++ b/models/system/appstate.go @@ -7,6 +7,8 @@ import ( "context" "gitea.dev/models/db" + + "xorm.io/builder" ) // AppState represents a state record in database @@ -44,9 +46,7 @@ func SaveAppStateContent(ctx context.Context, key, content string) error { // GetAppStateContent gets an app state from database func GetAppStateContent(ctx context.Context, key string) (content string, err error) { - e := db.GetEngine(ctx) - appState := &AppState{ID: key} - has, err := e.Get(appState) + appState, has, err := db.Get[AppState](ctx, builder.Eq{"id": key}) if err != nil { return "", err } else if !has { diff --git a/models/user/email_address.go b/models/user/email_address.go index 5a259b8e2b..7365f5374e 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -348,8 +348,8 @@ func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddres opts := &TimeLimitCodeOptions{Purpose: TimeLimitCodeActivateEmail, NewEmail: email} data := makeTimeLimitCodeHashData(opts, user) if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) { - emailAddress := &EmailAddress{UID: user.ID, Email: email} - if has, _ := db.GetEngine(ctx).Get(emailAddress); has { + emailAddress, has, _ := db.Get[EmailAddress](ctx, builder.Eq{"uid": user.ID, "email": email}) + if has { return emailAddress } } diff --git a/models/user/follow.go b/models/user/follow.go index 617211886e..11d293bc5e 100644 --- a/models/user/follow.go +++ b/models/user/follow.go @@ -8,6 +8,8 @@ import ( "gitea.dev/models/db" "gitea.dev/modules/timeutil" + + "xorm.io/builder" ) // Follow represents relations of user and their followers. @@ -24,7 +26,7 @@ func init() { // IsFollowing returns true if user is following followID. func IsFollowing(ctx context.Context, userID, followID int64) bool { - has, _ := db.GetEngine(ctx).Get(&Follow{UserID: userID, FollowID: followID}) + has, _ := db.Exist[Follow](ctx, builder.Eq{"user_id": userID, "follow_id": followID}) return has } diff --git a/models/user/openid.go b/models/user/openid.go index 84ec5a5cd8..dd4682b440 100644 --- a/models/user/openid.go +++ b/models/user/openid.go @@ -9,6 +9,8 @@ import ( "gitea.dev/models/db" "gitea.dev/modules/util" + + "xorm.io/builder" ) // UserOpenID is the list of all OpenID identities of a user. @@ -43,7 +45,7 @@ func isOpenIDUsed(ctx context.Context, uri string) (bool, error) { return true, nil } - return db.GetEngine(ctx).Get(&UserOpenID{URI: uri}) + return db.Exist[UserOpenID](ctx, builder.Eq{"uri": uri}) } // ErrOpenIDAlreadyUsed represents a "OpenIDAlreadyUsed" kind of error. diff --git a/models/user/redirect.go b/models/user/redirect.go index 325a643eb6..93a9c94c73 100644 --- a/models/user/redirect.go +++ b/models/user/redirect.go @@ -10,6 +10,8 @@ import ( "gitea.dev/models/db" "gitea.dev/modules/util" + + "xorm.io/builder" ) // ErrUserRedirectNotExist represents a "UserRedirectNotExist" kind of error. @@ -50,8 +52,8 @@ func init() { // LookupUserRedirect look up userID if a user has a redirect name func LookupUserRedirect(ctx context.Context, userName string) (int64, error) { userName = strings.ToLower(userName) - redirect := &Redirect{LowerName: userName} - if has, err := db.GetEngine(ctx).Get(redirect); err != nil { + redirect, has, err := db.Get[Redirect](ctx, builder.Eq{"lower_name": userName}) + if err != nil { return 0, err } else if !has { return 0, ErrUserRedirectNotExist{Name: userName} diff --git a/models/user/setting.go b/models/user/setting.go index a3903d5915..8888e3a2e0 100644 --- a/models/user/setting.go +++ b/models/user/setting.go @@ -125,8 +125,7 @@ func GetUserSetting(ctx context.Context, userID int64, key string, def ...string return "", err } - setting := &Setting{UserID: userID, SettingKey: key} - has, err := db.GetEngine(ctx).Get(setting) + setting, has, err := db.Get[Setting](ctx, builder.Eq{"user_id": userID, "setting_key": key}) if err != nil { return "", err } diff --git a/models/user/user.go b/models/user/user.go index 4e6227de9e..6d3d85c309 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1276,8 +1276,7 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) { email = strings.ToLower(email) // Otherwise, check in alternative list for activated email addresses - emailAddress := &EmailAddress{LowerEmail: email, IsActivated: true} - has, err := db.GetEngine(ctx).Get(emailAddress) + emailAddress, has, err := db.Get[EmailAddress](ctx, builder.Eq{"lower_email": email, "is_activated": true}) if err != nil { return nil, err } @@ -1297,13 +1296,29 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) { return nil, ErrUserNotExist{Name: email} } -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) +func GetIndividualUserByPrimaryEmail(ctx context.Context, email string) (*User, error) { + email = strings.ToLower(strings.TrimSpace(email)) + if len(email) == 0 { + return nil, ErrUserNotExist{Name: email} + } + + user, has, err := db.Get[User](ctx, builder.Eq{"email": email, "type": UserTypeIndividual}) + if err != nil { + return nil, err + } + if !has { + return nil, ErrUserNotExist{Name: email} + } + return user, nil +} + +func GetIndividualUserByLoginSource(ctx context.Context, loginType auth.Type, loginSource int64, loginName string) (*User, bool, error) { + user, has, err := db.Get[User](ctx, builder.Eq{"login_type": loginType, "login_source": loginSource, "login_name": loginName}) if has && user.Type != UserTypeIndividual { has = false + user = nil } - return has, err + return user, has, err } // GetUserByOpenID returns the user object by given OpenID if exists. diff --git a/modules/actions/jobparser/interpeter.go b/modules/actions/jobparser/interpeter.go index 37defeff7a..9294c717a1 100644 --- a/modules/actions/jobparser/interpeter.go +++ b/modules/actions/jobparser/interpeter.go @@ -57,9 +57,14 @@ func NewInterpeter( } ee := &exprparser.EvaluationEnvironment{ - Github: gitCtx, - Env: nil, // no need - Job: nil, // no need + Github: gitCtx, + Env: nil, // no need + // Job must be non-nil because cancelled() dereferences Job.Status unconditionally. + // See: https://gitea.com/gitea/runner/src/commit/ad967330a8788c9b8ab723abbc1a86d53c3bc5e6/act/exprparser/functions.go#L299 + // TODO: The empty JobContext.Status is right for now because Gitea never checks `if` condition when the workflow run is cancelled. + // This is an implementation gap in Gitea Actions. When a workflow run is cancelled, Gitea should check the job's `if` condition, + // and if the condition is met (e.g. `if: ${{ cancelled() }}` ), the job should be executed rather than cancelled. + Job: &model.JobContext{}, Steps: nil, // no need Runner: nil, // no need Secrets: nil, // no need diff --git a/modules/actions/jobparser/jobparser.go b/modules/actions/jobparser/jobparser.go index e7a2b48498..79c7b7b433 100644 --- a/modules/actions/jobparser/jobparser.go +++ b/modules/actions/jobparser/jobparser.go @@ -69,6 +69,9 @@ func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) { runsOn[i] = evaluator.Interpolate(v) } job.RawRunsOn = encodeRunsOn(runsOn) + if err := evaluator.EvaluateYamlNode(&job.RawContinueOnError); err != nil { + return nil, fmt.Errorf("evaluate continue-on-error for job %q: %w", id, err) + } swf := &SingleWorkflow{ Name: workflow.Name, RawOn: workflow.RawOn, diff --git a/modules/actions/jobparser/jobparser_test.go b/modules/actions/jobparser/jobparser_test.go index e74f0644f8..05bb3151b7 100644 --- a/modules/actions/jobparser/jobparser_test.go +++ b/modules/actions/jobparser/jobparser_test.go @@ -58,6 +58,11 @@ func TestParse(t *testing.T) { options: nil, wantErr: false, }, + { + name: "continue_on_error_expr", + options: nil, + wantErr: false, + }, } invalidFileTests := []struct { name string diff --git a/modules/actions/jobparser/model.go b/modules/actions/jobparser/model.go index c80626e4c0..97a36235d2 100644 --- a/modules/actions/jobparser/model.go +++ b/modules/actions/jobparser/model.go @@ -79,23 +79,37 @@ func (w *SingleWorkflow) Marshal() ([]byte, error) { } type Job struct { - Name string `yaml:"name,omitempty"` - RawNeeds yaml.Node `yaml:"needs,omitempty"` - RawRunsOn yaml.Node `yaml:"runs-on,omitempty"` - Env yaml.Node `yaml:"env,omitempty"` - If yaml.Node `yaml:"if,omitempty"` - Steps []*Step `yaml:"steps,omitempty"` - TimeoutMinutes string `yaml:"timeout-minutes,omitempty"` - Services map[string]*ContainerSpec `yaml:"services,omitempty"` - Strategy Strategy `yaml:"strategy,omitempty"` - RawContainer yaml.Node `yaml:"container,omitempty"` - Defaults Defaults `yaml:"defaults,omitempty"` - Outputs map[string]string `yaml:"outputs,omitempty"` - Uses string `yaml:"uses,omitempty"` - With map[string]any `yaml:"with,omitempty"` - RawSecrets yaml.Node `yaml:"secrets,omitempty"` - RawConcurrency *model.RawConcurrency `yaml:"concurrency,omitempty"` - RawPermissions yaml.Node `yaml:"permissions,omitempty"` + Name string `yaml:"name,omitempty"` + RawNeeds yaml.Node `yaml:"needs,omitempty"` + RawRunsOn yaml.Node `yaml:"runs-on,omitempty"` + Env yaml.Node `yaml:"env,omitempty"` + If yaml.Node `yaml:"if,omitempty"` + Steps []*Step `yaml:"steps,omitempty"` + TimeoutMinutes string `yaml:"timeout-minutes,omitempty"` + RawContinueOnError yaml.Node `yaml:"continue-on-error,omitempty"` + Services map[string]*ContainerSpec `yaml:"services,omitempty"` + Strategy Strategy `yaml:"strategy,omitempty"` + RawContainer yaml.Node `yaml:"container,omitempty"` + Defaults Defaults `yaml:"defaults,omitempty"` + Outputs map[string]string `yaml:"outputs,omitempty"` + Uses string `yaml:"uses,omitempty"` + With map[string]any `yaml:"with,omitempty"` + RawSecrets yaml.Node `yaml:"secrets,omitempty"` + RawConcurrency *model.RawConcurrency `yaml:"concurrency,omitempty"` + RawPermissions yaml.Node `yaml:"permissions,omitempty"` +} + +// GetContinueOnError decodes the continue-on-error field to a bool. +// The field may be a literal bool or an already-evaluated expression node. +func (j *Job) GetContinueOnError() bool { + if j.RawContinueOnError.Kind == 0 { + return false + } + var v bool + if err := j.RawContinueOnError.Decode(&v); err != nil { + return false + } + return v } func (j *Job) Clone() *Job { @@ -103,23 +117,24 @@ func (j *Job) Clone() *Job { return nil } return &Job{ - Name: j.Name, - RawNeeds: j.RawNeeds, - RawRunsOn: j.RawRunsOn, - Env: j.Env, - If: j.If, - Steps: j.Steps, - TimeoutMinutes: j.TimeoutMinutes, - Services: j.Services, - Strategy: j.Strategy, - RawContainer: j.RawContainer, - Defaults: j.Defaults, - Outputs: j.Outputs, - Uses: j.Uses, - With: j.With, - RawSecrets: j.RawSecrets, - RawConcurrency: j.RawConcurrency, - RawPermissions: j.RawPermissions, + Name: j.Name, + RawNeeds: j.RawNeeds, + RawRunsOn: j.RawRunsOn, + Env: j.Env, + If: j.If, + Steps: j.Steps, + TimeoutMinutes: j.TimeoutMinutes, + RawContinueOnError: j.RawContinueOnError, + Services: j.Services, + Strategy: j.Strategy, + RawContainer: j.RawContainer, + Defaults: j.Defaults, + Outputs: j.Outputs, + Uses: j.Uses, + With: j.With, + RawSecrets: j.RawSecrets, + RawConcurrency: j.RawConcurrency, + RawPermissions: j.RawPermissions, } } diff --git a/modules/actions/jobparser/model_test.go b/modules/actions/jobparser/model_test.go index 567c160f92..e3b2d2b95f 100644 --- a/modules/actions/jobparser/model_test.go +++ b/modules/actions/jobparser/model_test.go @@ -336,6 +336,52 @@ func TestSingleWorkflow_SetJob(t *testing.T) { }) } +func TestGetContinueOnError(t *testing.T) { + tests := []struct { + name string + yaml string + want bool + }{ + { + name: "absent", + yaml: "name: test\non: push\njobs:\n job1:\n runs-on: ubuntu-22.04\n steps:\n - run: echo hi\n", + want: false, + }, + { + name: "static true", + yaml: "name: test\non: push\njobs:\n job1:\n runs-on: ubuntu-22.04\n continue-on-error: true\n steps:\n - run: echo hi\n", + want: true, + }, + { + name: "static false", + yaml: "name: test\non: push\njobs:\n job1:\n runs-on: ubuntu-22.04\n continue-on-error: false\n steps:\n - run: echo hi\n", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Parse([]byte(tt.yaml)) + require.NoError(t, err) + require.Len(t, got, 1) + _, job := got[0].Job() + assert.Equal(t, tt.want, job.GetContinueOnError()) + }) + } + + // Expression case: ${{ matrix.experimental }} must resolve per matrix variant. + t.Run("matrix expression", func(t *testing.T) { + content := ReadTestdata(t, "continue_on_error_expr.in.yaml") + got, err := Parse(content) + require.NoError(t, err) + require.Len(t, got, 2) + // sorted by matrix name: (false) before (true) + _, jobFalse := got[0].Job() + _, jobTrue := got[1].Job() + assert.False(t, jobFalse.GetContinueOnError()) + assert.True(t, jobTrue.GetContinueOnError()) + }) +} + func TestParseMappingNode(t *testing.T) { tests := []struct { input string @@ -418,3 +464,61 @@ func TestParseMappingNode(t *testing.T) { }) } } + +func TestEvaluateJobIfExpression(t *testing.T) { + kases := []struct { + name string + ifCond string + needResult string + expected bool + }{ + {name: "empty need success", ifCond: "${{ 1 == 1 }}", needResult: "success", expected: true}, + {name: "always", ifCond: "${{ always() }}", needResult: "failure", expected: true}, + {name: "failure true", ifCond: "${{ failure() }}", needResult: "failure", expected: true}, + {name: "failure false", ifCond: "${{ failure() }}", needResult: "success", expected: false}, + {name: "success true", ifCond: "${{ success() }}", needResult: "success", expected: true}, + // cancelled() is always false on the server: a cancelled run never evaluates a blocked job's `if:` + {name: "cancelled", ifCond: "${{ cancelled() }}", needResult: "success", expected: false}, + {name: "not cancelled or failure", ifCond: "${{ !(cancelled() || failure()) }}", needResult: "success", expected: true}, + {name: "not cancelled or failure, need failed", ifCond: "${{ !(cancelled() || failure()) }}", needResult: "failure", expected: false}, + } + for _, kase := range kases { + t.Run(kase.name, func(t *testing.T) { + content := strings.ReplaceAll(` +name: test +on: push +jobs: + job1: + runs-on: ubuntu-latest + steps: + - run: echo job1 + job2: + runs-on: ubuntu-latest + needs: [job1] + if: IF_COND + steps: + - run: echo job2 +`, "IF_COND", kase.ifCond) + + workflows, err := Parse([]byte(content)) + require.NoError(t, err) + + var job2 *Job + for _, wf := range workflows { + if id, job := wf.Job(); id == "job2" { + job2 = job + } + } + require.NotNil(t, job2) + + // mirrors findJobNeedsAndFillJobResults: the needs' results plus a self entry carrying Needs + results := map[string]*JobResult{ + "job1": {Result: kase.needResult}, + "job2": {Needs: []string{"job1"}}, + } + got, err := EvaluateJobIfExpression("job2", job2, map[string]any{}, results, nil, nil) + require.NoError(t, err) + assert.Equal(t, kase.expected, got) + }) + } +} diff --git a/modules/actions/jobparser/testdata/continue_on_error_expr.in.yaml b/modules/actions/jobparser/testdata/continue_on_error_expr.in.yaml new file mode 100644 index 0000000000..5dc55d38eb --- /dev/null +++ b/modules/actions/jobparser/testdata/continue_on_error_expr.in.yaml @@ -0,0 +1,10 @@ +name: test +jobs: + job1: + strategy: + matrix: + experimental: [false, true] + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental }} + steps: + - run: echo hi diff --git a/modules/actions/jobparser/testdata/continue_on_error_expr.out.yaml b/modules/actions/jobparser/testdata/continue_on_error_expr.out.yaml new file mode 100644 index 0000000000..0d3afff878 --- /dev/null +++ b/modules/actions/jobparser/testdata/continue_on_error_expr.out.yaml @@ -0,0 +1,25 @@ +name: test +jobs: + job1: + name: job1 (false) + runs-on: ubuntu-22.04 + steps: + - run: echo hi + continue-on-error: false + strategy: + matrix: + experimental: + - false +--- +name: test +jobs: + job1: + name: job1 (true) + runs-on: ubuntu-22.04 + steps: + - run: echo hi + continue-on-error: true + strategy: + matrix: + experimental: + - true diff --git a/modules/actions/jobparser/uses.go b/modules/actions/jobparser/uses.go index d829537c69..3d0e3d44f9 100644 --- a/modules/actions/jobparser/uses.go +++ b/modules/actions/jobparser/uses.go @@ -15,9 +15,11 @@ import ( type UsesKind int const ( - // UsesKindLocalSameRepo is "./.gitea/workflows/foo.yml" - a path inside the calling repository. + // UsesKindLocalSameRepo is ".//foo.yml" - a path inside the calling repository. + // For example: "./.gitea/workflows/foo.yml" UsesKindLocalSameRepo UsesKind = iota + 1 - // UsesKindLocalCrossRepo is "owner/repo/.gitea/workflows/foo.yml@ref" - a workflow in another repo on the same instance. + // UsesKindLocalCrossRepo is "owner/repo//foo.yml@ref" - a workflow in another repo on the same instance. + // For example: "owner/repo/.gitea/workflows/foo.yml@ref" UsesKindLocalCrossRepo ) @@ -31,14 +33,16 @@ type UsesRef struct { } var ( - reLocalSameRepo = regexp.MustCompile(`^\./\.(gitea|github)/workflows/([^@]+\.ya?ml)$`) - reLocalCrossRepo = regexp.MustCompile(`^([-.\w]+)/([-.\w]+)/\.(gitea|github)/workflows/([^@]+\.ya?ml)@(.+)$`) + reLocalSameRepo = regexp.MustCompile(`^\./([^@]+\.ya?ml)$`) + reLocalCrossRepo = regexp.MustCompile(`^([-.\w]+)/([-.\w]+)/([^@]+\.ya?ml)@(.+)$`) ) -// ParseUses parses a reusable workflow "uses:" value. -// Only two forms are supported: -// - "./.gitea/workflows/foo.yml" (UsesKindLocalSameRepo, no @ref) -// - "OWNER/REPO/.gitea/workflows/foo.yml@REF" (UsesKindLocalCrossRepo) +// ParseUses parses the SYNTAX of a reusable workflow "uses:" value into a UsesRef. Two forms are supported: +// - ".//foo.yml" (UsesKindLocalSameRepo, no @ref) +// - "OWNER/REPO//foo.yml@REF" (UsesKindLocalCrossRepo) +// +// It deliberately does NOT validate that is an allowed workflow directory: the allowed directories are instance-configurable (WORKFLOW_DIRS / SCOPED_WORKFLOW_DIRS). +// The caller (services/actions.ResolveUses) enforces the directory allowlist. The returned Path is the cleaned, repo-relative file path. func ParseUses(s string) (*UsesRef, error) { s = strings.TrimSpace(s) if s == "" { @@ -48,9 +52,9 @@ func ParseUses(s string) (*UsesRef, error) { if strings.HasPrefix(s, "./") { m := reLocalSameRepo.FindStringSubmatch(s) if m == nil { - return nil, fmt.Errorf(`invalid local "uses:" %q (expect ./.gitea/workflows/.yml)`, s) + return nil, fmt.Errorf(`invalid local "uses:" %q (expect .//.yml)`, s) } - p := fmt.Sprintf(".%s/workflows/%s", m[1], m[2]) + p := m[1] if path.Clean(p) != p { return nil, fmt.Errorf("invalid workflow path %q", s) } @@ -59,9 +63,9 @@ func ParseUses(s string) (*UsesRef, error) { m := reLocalCrossRepo.FindStringSubmatch(s) if m == nil { - return nil, fmt.Errorf(`invalid cross-repo "uses:" %q (expect owner/repo/.gitea/workflows/.yml@ref)`, s) + return nil, fmt.Errorf(`invalid cross-repo "uses:" %q (expect owner/repo//.yml@ref)`, s) } - p := fmt.Sprintf(".%s/workflows/%s", m[3], m[4]) + p := m[3] if path.Clean(p) != p { return nil, fmt.Errorf("invalid workflow path %q", s) } @@ -70,6 +74,6 @@ func ParseUses(s string) (*UsesRef, error) { Owner: m[1], Repo: m[2], Path: p, - Ref: m[5], + Ref: m[4], }, nil } diff --git a/modules/actions/jobparser/uses_test.go b/modules/actions/jobparser/uses_test.go index c6692054dc..01f76d67de 100644 --- a/modules/actions/jobparser/uses_test.go +++ b/modules/actions/jobparser/uses_test.go @@ -42,6 +42,17 @@ func TestParseUses(t *testing.T) { in: "./.gitea/workflows/sub/build.yml", want: UsesRef{Kind: UsesKindLocalSameRepo, Path: ".gitea/workflows/sub/build.yml"}, }, + { + // ParseUses is dir-agnostic; the allowed directories (WORKFLOW_DIRS / SCOPED_WORKFLOW_DIRS) are enforced by ResolveUses. + name: "scoped workflows dir parses", + in: "./.gitea/scoped_workflows/lib.yml", + want: UsesRef{Kind: UsesKindLocalSameRepo, Path: ".gitea/scoped_workflows/lib.yml"}, + }, + { + name: "non-default dir parses (allowlist enforced downstream)", + in: "./.gitea/custom_workflows/x.yaml", + want: UsesRef{Kind: UsesKindLocalSameRepo, Path: ".gitea/custom_workflows/x.yaml"}, + }, { name: "leading/trailing whitespace is trimmed", in: " ./.gitea/workflows/build.yml ", @@ -118,6 +129,17 @@ func TestParseUses(t *testing.T) { Ref: "v1", }, }, + { + name: "scoped workflows dir parses (allowlist enforced by ResolveUses)", + in: "owner/repo/.gitea/scoped_workflows/lib.yml@v1", + want: UsesRef{ + Kind: UsesKindLocalCrossRepo, + Owner: "owner", + Repo: "repo", + Path: ".gitea/scoped_workflows/lib.yml", + Ref: "v1", + }, + }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -136,23 +158,20 @@ func TestParseUses(t *testing.T) { {name: "empty string", in: ""}, {name: "whitespace only", in: " "}, - // Same-repo malformed + // Same-repo malformed (note: a wrong *directory* parses and should be rejected by the caller) {name: "same-repo with @ref", in: "./.gitea/workflows/build.yml@v1"}, - {name: "same-repo wrong directory", in: "./not-workflows/build.yml"}, {name: "same-repo wrong extension", in: "./.gitea/workflows/build.txt"}, {name: "same-repo missing extension", in: "./.gitea/workflows/build"}, {name: "same-repo absolute path", in: "/.gitea/workflows/build.yml"}, {name: "same-repo path traversal", in: "./.gitea/workflows/../escape.yml"}, {name: "same-repo double slash", in: "./.gitea/workflows//build.yml"}, {name: "same-repo redundant ./", in: "./.gitea/workflows/./build.yml"}, - {name: "same-repo no filename", in: "./.gitea/workflows/.yml"}, // Cross-repo malformed {name: "cross-repo missing @ref", in: "owner/repo/.gitea/workflows/build.yml"}, {name: "cross-repo empty ref", in: "owner/repo/.gitea/workflows/build.yml@"}, {name: "cross-repo missing owner", in: "/repo/.gitea/workflows/build.yml@v1"}, {name: "cross-repo missing repo", in: "owner//.gitea/workflows/build.yml@v1"}, - {name: "cross-repo wrong workflows dir", in: "owner/repo/workflows/build.yml@v1"}, {name: "cross-repo wrong extension", in: "owner/repo/.gitea/workflows/build.txt@v1"}, {name: "cross-repo path traversal", in: "owner/repo/.gitea/workflows/../escape.yml@v1"}, {name: "cross-repo double slash in path", in: "owner/repo/.gitea/workflows//build.yml@v1"}, diff --git a/modules/actions/scoped_workflows.go b/modules/actions/scoped_workflows.go new file mode 100644 index 0000000000..3e3cd47da9 --- /dev/null +++ b/modules/actions/scoped_workflows.go @@ -0,0 +1,91 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + "gitea.dev/modules/actions/jobparser" + "gitea.dev/modules/git" + "gitea.dev/modules/log" + "gitea.dev/modules/setting" + api "gitea.dev/modules/structs" + webhook_module "gitea.dev/modules/webhook" +) + +// ListScopedWorkflows lists scoped workflow files (under SCOPED_WORKFLOW_DIRS) at the given commit. +func ListScopedWorkflows(ctx context.Context, gitRepo *git.Repository, commit *git.Commit) (string, git.Entries, error) { + return listWorkflowsInDirs(ctx, gitRepo, commit, setting.Actions.ScopedWorkflowDirs) +} + +// ParsedScopedWorkflow is one scoped workflow's source-side parse result +type ParsedScopedWorkflow struct { + EntryName string + DisplayName string // the workflow `name:` or base file name + Content []byte // raw content of the workflow file + Events []*jobparser.Event // decoded `on:` events +} + +// ParseScopedWorkflows lists and parses the scoped workflow files at sourceCommit (under SCOPED_WORKFLOW_DIRS). +func ParseScopedWorkflows(ctx context.Context, gitRepo *git.Repository, sourceCommit *git.Commit) ([]*ParsedScopedWorkflow, error) { + _, entries, err := ListScopedWorkflows(ctx, gitRepo, sourceCommit) + if err != nil { + return nil, err + } + + parsed := make([]*ParsedScopedWorkflow, 0, len(entries)) + for _, entry := range entries { + content, err := GetContentFromEntry(ctx, gitRepo, entry) + if err != nil { + return nil, err + } + + // one workflow may have multiple events + events, err := GetEventsFromContent(content) + if err != nil { + log.Warn("ignore invalid scoped workflow %q: %v", entry.Name(), err) + continue + } + parsed = append(parsed, &ParsedScopedWorkflow{ + EntryName: entry.Name(), + DisplayName: WorkflowDisplayName(entry.Name(), content), + Content: content, + Events: events, + }) + } + return parsed, nil +} + +// MatchScopedWorkflows evaluates already-parsed scoped workflows against one consuming event. +// It returns the workflows whose `on:` matches, and those that matched the event but were excluded by a branch/paths filter (filtered). +func MatchScopedWorkflows( + ctx context.Context, + parsed []*ParsedScopedWorkflow, + consumerGitRepo *git.Repository, + consumerCommit *git.Commit, + triggedEvent webhook_module.HookEventType, + payload api.Payloader, +) (matched, filtered []*DetectedWorkflow) { + for _, p := range parsed { + for _, evt := range p.Events { + if evt.IsSchedule() { + // schedule is a non-target for scoped workflows + continue + } + dwf := &DetectedWorkflow{ + EntryName: p.EntryName, + TriggerEvent: evt, + Content: p.Content, + } + switch detectWorkflowMatch(ctx, consumerGitRepo, consumerCommit, triggedEvent, payload, evt) { + case detectMatched: + matched = append(matched, dwf) + case detectFilteredOut: + filtered = append(filtered, dwf) + case detectNotApplicable: + } + } + } + return matched, filtered +} diff --git a/modules/actions/scoped_workflows_test.go b/modules/actions/scoped_workflows_test.go new file mode 100644 index 0000000000..ae7085d26e --- /dev/null +++ b/modules/actions/scoped_workflows_test.go @@ -0,0 +1,62 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsWorkflowInDirs(t *testing.T) { + tests := []struct { + name string + dirs []string + path string + expected bool + }{ + { + name: "default scoped dir with yml", + dirs: []string{".gitea/scoped_workflows", ".github/scoped_workflows"}, + path: ".gitea/scoped_workflows/security.yml", + expected: true, + }, + { + name: "default scoped dir with yaml", + dirs: []string{".gitea/scoped_workflows", ".github/scoped_workflows"}, + path: ".github/scoped_workflows/lint.yaml", + expected: true, + }, + { + name: "normal workflow path is not a scoped workflow", + dirs: []string{".gitea/scoped_workflows"}, + path: ".gitea/workflows/ci.yml", + expected: false, + }, + { + name: "non-yaml file", + dirs: []string{".gitea/scoped_workflows"}, + path: ".gitea/scoped_workflows/readme.md", + expected: false, + }, + { + name: "feature disabled (no scoped dirs)", + dirs: []string{}, + path: ".gitea/scoped_workflows/security.yml", + expected: false, + }, + { + name: "directory boundary", + dirs: []string{".gitea/scoped_workflows"}, + path: ".gitea/scoped_workflows2/security.yml", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, isWorkflowInDirs(tt.path, tt.dirs)) + }) + } +} diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 28dc45dc7a..aabf95c44b 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -5,6 +5,9 @@ package actions import ( "bytes" + "context" + "fmt" + "path" "slices" "strings" @@ -28,6 +31,14 @@ type DetectedWorkflow struct { Content []byte } +type detectResult int + +const ( + detectMatched detectResult = iota // event matched; run normally + detectNotApplicable // event/type doesn't apply; create nothing + detectFilteredOut // matched but excluded by a branch/paths filter; posts a skipped commit status when the context is a required check +) + func init() { model.OnDecodeNodeError = func(node yaml.Node, out any, err error) { // Log the error instead of panic or fatal. @@ -38,11 +49,20 @@ func init() { } func IsWorkflow(path string) bool { + return isWorkflowInDirs(path, setting.Actions.WorkflowDirs) +} + +// IsWorkflowOrScopedWorkflow reports whether path is a workflow file under WORKFLOW_DIRS or SCOPED_WORKFLOW_DIRS. +func IsWorkflowOrScopedWorkflow(path string) bool { + return isWorkflowInDirs(path, setting.Actions.WorkflowDirs) || isWorkflowInDirs(path, setting.Actions.ScopedWorkflowDirs) +} + +func isWorkflowInDirs(path string, dirs []string) bool { if (!strings.HasSuffix(path, ".yaml")) && (!strings.HasSuffix(path, ".yml")) { return false } - for _, workflowDir := range setting.Actions.WorkflowDirs { + for _, workflowDir := range dirs { if strings.HasPrefix(path, workflowDir+"/") { return true } @@ -50,12 +70,16 @@ func IsWorkflow(path string) bool { return false } -func ListWorkflows(commit *git.Commit) (string, git.Entries, error) { +func ListWorkflows(ctx context.Context, gitRepo *git.Repository, commit *git.Commit) (string, git.Entries, error) { + return listWorkflowsInDirs(ctx, gitRepo, commit, setting.Actions.WorkflowDirs) +} + +func listWorkflowsInDirs(ctx context.Context, gitRepo *git.Repository, commit *git.Commit, dirs []string) (string, git.Entries, error) { var tree *git.Tree var err error var workflowDir string - for _, workflowDir = range setting.Actions.WorkflowDirs { - tree, err = commit.SubTree(workflowDir) + for _, workflowDir = range dirs { + tree, err = commit.SubTree(ctx, gitRepo, workflowDir) if err == nil { break } @@ -67,7 +91,7 @@ func ListWorkflows(commit *git.Commit) (string, git.Entries, error) { return "", nil, nil } - entries, err := tree.ListEntriesRecursiveFast() + entries, err := tree.ListEntriesRecursiveFast(ctx, gitRepo) if err != nil { return "", nil, err } @@ -81,8 +105,8 @@ func ListWorkflows(commit *git.Commit) (string, git.Entries, error) { return workflowDir, ret, nil } -func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) { - f, err := entry.Blob().DataAsync() +func GetContentFromEntry(ctx context.Context, gitRepo *git.Repository, entry *git.TreeEntry) ([]byte, error) { + f, err := entry.Blob(gitRepo).DataAsync(ctx) if err != nil { return nil, err } @@ -117,24 +141,57 @@ func ValidateWorkflowContent(content []byte) error { return err } +// WorkflowDisplayName returns a workflow's display name: its `name:` if non-blank, otherwise the base file name. +// This is the value used as the workflow segment of its commit-status context. +func WorkflowDisplayName(file string, content []byte) string { + displayName := path.Base(file) + if wfs, err := jobparser.Parse(content); err == nil && len(wfs) > 0 { + if name := strings.TrimSpace(wfs[0].Name); name != "" { + displayName = name + } + } + return displayName +} + +// WorkflowStatusContextName builds a workflow job's commit-status context name: " / ()". +func WorkflowStatusContextName(displayName, jobName, event string) string { + return strings.TrimSpace(fmt.Sprintf("%s / %s (%s)", displayName, jobName, event)) +} + +// ScopedWorkflowStatusContextName prefixes a scoped run's status-check context with its source repo, set off by a colon: ": / ()". +func ScopedWorkflowStatusContextName(prefix, displayName, jobName, event string) string { + return strings.TrimSpace(fmt.Sprintf("%s: %s", prefix, WorkflowStatusContextName(displayName, jobName, event))) +} + +// ShouldEventCreateCommitStatus reports whether a run triggered by the given workflow `on:` event posts a commit status, +// so its context can serve as a required status check. +// TODO: this allowlist duplicates the truth in services/actions.getCommitStatusEventNameAndCommitID, which decides the actual event string and whether a status is posted. +// The two are kept in sync by hand and can drift; unify them into a single source so adding a status-producing event in one place automatically updates the other. +func ShouldEventCreateCommitStatus(event string) bool { + switch event { + case "push", "pull_request", "pull_request_target", "pull_request_review", "pull_request_review_comment", "release": + return true + } + return false +} + func DetectWorkflows( + ctx context.Context, gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, detectSchedule bool, -) ([]*DetectedWorkflow, []*DetectedWorkflow, error) { - _, entries, err := ListWorkflows(commit) +) (workflows, schedules, filtered []*DetectedWorkflow, err error) { + _, entries, err := ListWorkflows(ctx, gitRepo, commit) if err != nil { - return nil, nil, err + return nil, nil, nil, err } - workflows := make([]*DetectedWorkflow, 0, len(entries)) - schedules := make([]*DetectedWorkflow, 0, len(entries)) for _, entry := range entries { - content, err := GetContentFromEntry(entry) + content, err := GetContentFromEntry(ctx, gitRepo, entry) if err != nil { - return nil, nil, err + return nil, nil, nil, err } // one workflow may have multiple events @@ -154,29 +211,35 @@ func DetectWorkflows( } schedules = append(schedules, dwf) } - } else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) { + } else { dwf := &DetectedWorkflow{ EntryName: entry.Name(), TriggerEvent: evt, Content: content, } - workflows = append(workflows, dwf) + switch detectWorkflowMatch(ctx, gitRepo, commit, triggedEvent, payload, evt) { + case detectMatched: + workflows = append(workflows, dwf) + case detectFilteredOut: + filtered = append(filtered, dwf) + case detectNotApplicable: + } } } } - return workflows, schedules, nil + return workflows, schedules, filtered, nil } -func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) { - _, entries, err := ListWorkflows(commit) +func DetectScheduledWorkflows(ctx context.Context, gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) { + _, entries, err := ListWorkflows(ctx, gitRepo, commit) if err != nil { return nil, err } wfs := make([]*DetectedWorkflow, 0, len(entries)) for _, entry := range entries { - content, err := GetContentFromEntry(entry) + content, err := GetContentFromEntry(ctx, gitRepo, entry) if err != nil { return nil, err } @@ -203,9 +266,9 @@ func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*D return wfs, nil } -func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool { +func detectWorkflowMatch(ctx context.Context, gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) detectResult { if !canGithubEventMatch(evt.Name, triggedEvent) { - return false + return detectNotApplicable } switch triggedEvent { @@ -219,25 +282,31 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts()) } // no special filter parameters for these events, just return true if name matched - return true + return detectMatched case // push webhook_module.HookEventPush: - return matchPushEvent(commit, payload.(*api.PushPayload), evt) + return matchPushEvent(ctx, gitRepo, commit, payload.(*api.PushPayload), evt) case // issues webhook_module.HookEventIssues, webhook_module.HookEventIssueAssign, webhook_module.HookEventIssueLabel, webhook_module.HookEventIssueMilestone: - return matchIssuesEvent(payload.(*api.IssuePayload), evt) + if matchIssuesEvent(payload.(*api.IssuePayload), evt) { + return detectMatched + } + return detectNotApplicable case // issue_comment webhook_module.HookEventIssueComment, // `pull_request_comment` is same as `issue_comment` // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment webhook_module.HookEventPullRequestComment: - return matchIssueCommentEvent(payload.(*api.IssueCommentPayload), evt) + if matchIssueCommentEvent(payload.(*api.IssueCommentPayload), evt) { + return detectMatched + } + return detectNotApplicable case // pull_request webhook_module.HookEventPullRequest, @@ -246,39 +315,54 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web webhook_module.HookEventPullRequestLabel, webhook_module.HookEventPullRequestReviewRequest, webhook_module.HookEventPullRequestMilestone: - return matchPullRequestEvent(gitRepo, commit, payload.(*api.PullRequestPayload), evt) + return matchPullRequestEvent(ctx, gitRepo, commit, payload.(*api.PullRequestPayload), evt) case // pull_request_review webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected: - return matchPullRequestReviewEvent(payload.(*api.PullRequestPayload), evt) + if matchPullRequestReviewEvent(payload.(*api.PullRequestPayload), evt) { + return detectMatched + } + return detectNotApplicable case // pull_request_review_comment webhook_module.HookEventPullRequestReviewComment: - return matchPullRequestReviewCommentEvent(payload.(*api.PullRequestPayload), evt) + if matchPullRequestReviewCommentEvent(payload.(*api.PullRequestPayload), evt) { + return detectMatched + } + return detectNotApplicable case // release webhook_module.HookEventRelease: - return matchReleaseEvent(payload.(*api.ReleasePayload), evt) + if matchReleaseEvent(payload.(*api.ReleasePayload), evt) { + return detectMatched + } + return detectNotApplicable case // registry_package webhook_module.HookEventPackage: - return matchPackageEvent(payload.(*api.PackagePayload), evt) + if matchPackageEvent(payload.(*api.PackagePayload), evt) { + return detectMatched + } + return detectNotApplicable case // workflow_run webhook_module.HookEventWorkflowRun: - return matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt) + if matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt) { + return detectMatched + } + return detectNotApplicable default: log.Warn("unsupported event %q", triggedEvent) - return false + return detectNotApplicable } } -func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobparser.Event) bool { +func matchPushEvent(ctx context.Context, gitRepo *git.Repository, commit *git.Commit, pushPayload *api.PushPayload, evt *jobparser.Event) detectResult { // with no special filter parameters if len(evt.Acts()) == 0 { - return true + return detectMatched } matchTimes := 0 @@ -341,34 +425,34 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa matchTimes++ break } - filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) + filesChanged, err := commit.GetFilesChangedSinceCommit(ctx, gitRepo, pushPayload.Before) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) - } else { - patterns, err := workflowpattern.CompilePatterns(vals...) - if err != nil { - break - } - if !workflowpattern.Skip(patterns, filesChanged) { - matchTimes++ - } + return detectNotApplicable + } + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Skip(patterns, filesChanged) { + matchTimes++ } case "paths-ignore": if refName.IsTag() { matchTimes++ break } - filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) + filesChanged, err := commit.GetFilesChangedSinceCommit(ctx, gitRepo, pushPayload.Before) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) - } else { - patterns, err := workflowpattern.CompilePatterns(vals...) - if err != nil { - break - } - if !workflowpattern.Filter(patterns, filesChanged) { - matchTimes++ - } + return detectNotApplicable + } + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Filter(patterns, filesChanged) { + matchTimes++ } default: log.Warn("push event unsupported condition %q", cond) @@ -378,7 +462,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa if hasBranchFilter && hasTagFilter { matchTimes++ } - return matchTimes == len(evt.Acts()) + if matchTimes == len(evt.Acts()) { + return detectMatched + } + return detectFilteredOut } func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool { @@ -429,7 +516,7 @@ func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool return matchTimes == len(evt.Acts()) } -func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) bool { +func matchPullRequestEvent(ctx context.Context, gitRepo *git.Repository, commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) detectResult { acts := evt.Acts() activityTypeMatched := false matchTimes := 0 @@ -473,10 +560,10 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa err error ) if evt.Name == GithubEventPullRequestTarget && (len(acts["paths"]) > 0 || len(acts["paths-ignore"]) > 0) { - headCommit, err = gitRepo.GetCommit(prPayload.PullRequest.Head.Sha) + headCommit, err = gitRepo.GetCommit(ctx, prPayload.PullRequest.Head.Sha) if err != nil { log.Error("GetCommit [ref: %s]: %v", prPayload.PullRequest.Head.Sha, err) - return false + return detectNotApplicable } } @@ -505,36 +592,42 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa matchTimes++ } case "paths": - filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase) + filesChanged, err := headCommit.GetFilesChangedSinceCommit(ctx, gitRepo, prPayload.PullRequest.MergeBase) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err) - } else { - patterns, err := workflowpattern.CompilePatterns(vals...) - if err != nil { - break - } - if !workflowpattern.Skip(patterns, filesChanged) { - matchTimes++ - } + return detectNotApplicable + } + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Skip(patterns, filesChanged) { + matchTimes++ } case "paths-ignore": - filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase) + filesChanged, err := headCommit.GetFilesChangedSinceCommit(ctx, gitRepo, prPayload.PullRequest.MergeBase) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err) - } else { - patterns, err := workflowpattern.CompilePatterns(vals...) - if err != nil { - break - } - if !workflowpattern.Filter(patterns, filesChanged) { - matchTimes++ - } + return detectNotApplicable + } + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Filter(patterns, filesChanged) { + matchTimes++ } default: log.Warn("pull request event unsupported condition %q", cond) } } - return activityTypeMatched && matchTimes == len(evt.Acts()) + if !activityTypeMatched { + return detectNotApplicable + } + if matchTimes != len(evt.Acts()) { + return detectFilteredOut + } + return detectMatched } func matchIssueCommentEvent(issueCommentPayload *api.IssueCommentPayload, evt *jobparser.Event) bool { diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index aec7144783..190d7e1595 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -101,49 +101,49 @@ func TestDetectMatched(t *testing.T) { triggedEvent webhook_module.HookEventType payload api.Payloader yamlOn string - expected bool + expected detectResult }{ { desc: "HookEventCreate(create) matches GithubEventCreate(create)", triggedEvent: webhook_module.HookEventCreate, payload: nil, yamlOn: "on: create", - expected: true, + expected: detectMatched, }, { desc: "HookEventIssues(issues) `opened` action matches GithubEventIssues(issues)", triggedEvent: webhook_module.HookEventIssues, payload: &api.IssuePayload{Action: api.HookIssueOpened}, yamlOn: "on: issues", - expected: true, + expected: detectMatched, }, { desc: "HookEventIssues(issues) `milestoned` action matches GithubEventIssues(issues)", triggedEvent: webhook_module.HookEventIssues, payload: &api.IssuePayload{Action: api.HookIssueMilestoned}, yamlOn: "on: issues", - expected: true, + expected: detectMatched, }, { desc: "HookEventPullRequestSync(pull_request_sync) matches GithubEventPullRequest(pull_request)", triggedEvent: webhook_module.HookEventPullRequestSync, payload: &api.PullRequestPayload{Action: api.HookIssueSynchronized}, yamlOn: "on: pull_request", - expected: true, + expected: detectMatched, }, { desc: "HookEventPullRequest(pull_request) `label_updated` action doesn't match GithubEventPullRequest(pull_request) with no activity type", triggedEvent: webhook_module.HookEventPullRequest, payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated}, yamlOn: "on: pull_request", - expected: false, + expected: detectNotApplicable, }, { desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with no activity type", triggedEvent: webhook_module.HookEventPullRequest, payload: &api.PullRequestPayload{Action: api.HookIssueClosed}, yamlOn: "on: pull_request", - expected: false, + expected: detectNotApplicable, }, { desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with branches", @@ -155,56 +155,56 @@ func TestDetectMatched(t *testing.T) { }, }, yamlOn: "on:\n pull_request:\n branches: [main]", - expected: false, + expected: detectNotApplicable, }, { desc: "HookEventPullRequest(pull_request) `label_updated` action matches GithubEventPullRequest(pull_request) with `label` activity type", triggedEvent: webhook_module.HookEventPullRequest, payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated}, yamlOn: "on:\n pull_request:\n types: [labeled]", - expected: true, + expected: detectMatched, }, { desc: "HookEventPullRequestReviewComment(pull_request_review_comment) matches GithubEventPullRequestReviewComment(pull_request_review_comment)", triggedEvent: webhook_module.HookEventPullRequestReviewComment, payload: &api.PullRequestPayload{Action: api.HookIssueReviewed}, yamlOn: "on:\n pull_request_review_comment:\n types: [created]", - expected: true, + expected: detectMatched, }, { desc: "HookEventPullRequestReviewRejected(pull_request_review_rejected) doesn't match GithubEventPullRequestReview(pull_request_review) with `dismissed` activity type (we don't support `dismissed` at present)", triggedEvent: webhook_module.HookEventPullRequestReviewRejected, payload: &api.PullRequestPayload{Action: api.HookIssueReviewed}, yamlOn: "on:\n pull_request_review:\n types: [dismissed]", - expected: false, + expected: detectNotApplicable, }, { desc: "HookEventRelease(release) `published` action matches GithubEventRelease(release) with `published` activity type", triggedEvent: webhook_module.HookEventRelease, payload: &api.ReleasePayload{Action: api.HookReleasePublished}, yamlOn: "on:\n release:\n types: [published]", - expected: true, + expected: detectMatched, }, { desc: "HookEventPackage(package) `created` action doesn't match GithubEventRegistryPackage(registry_package) with `updated` activity type", triggedEvent: webhook_module.HookEventPackage, payload: &api.PackagePayload{Action: api.HookPackageCreated}, yamlOn: "on:\n registry_package:\n types: [updated]", - expected: false, + expected: detectNotApplicable, }, { desc: "HookEventWiki(wiki) matches GithubEventGollum(gollum)", triggedEvent: webhook_module.HookEventWiki, payload: nil, yamlOn: "on: gollum", - expected: true, + expected: detectMatched, }, { desc: "HookEventSchedule(schedule) matches GithubEventSchedule(schedule)", triggedEvent: webhook_module.HookEventSchedule, payload: nil, yamlOn: "on: schedule", - expected: true, + expected: detectMatched, }, { desc: "push to tag matches workflow with paths condition (should skip paths check)", @@ -222,7 +222,19 @@ func TestDetectMatched(t *testing.T) { }, commit: nil, yamlOn: "on:\n push:\n paths:\n - src/**", - expected: true, + expected: detectMatched, + }, + { + desc: "push branch filter excludes -> filtered out", + triggedEvent: webhook_module.HookEventPush, + payload: &api.PushPayload{ + Ref: "refs/heads/feature/x", + Before: "0000000", + Commits: []*api.PayloadCommit{{ID: "abc", Added: []string{"a.go"}, Message: "x"}}, + }, + commit: nil, + yamlOn: "on:\n push:\n branches: [main]", + expected: detectFilteredOut, }, } @@ -231,7 +243,7 @@ func TestDetectMatched(t *testing.T) { 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])) + assert.Equal(t, tc.expected, detectWorkflowMatch(t.Context(), nil, tc.commit, tc.triggedEvent, tc.payload, evts[0])) }) } } diff --git a/modules/auth/openid/openid.go b/modules/auth/openid/openid.go index 249ce02335..c0ecd6ddb5 100644 --- a/modules/auth/openid/openid.go +++ b/modules/auth/openid/openid.go @@ -4,8 +4,14 @@ package openid import ( + "net/http" + "sync" "time" + "gitea.dev/modules/hostmatcher" + "gitea.dev/modules/proxy" + "gitea.dev/modules/setting" + "github.com/yohcop/openid-go" ) @@ -19,11 +25,23 @@ import ( var ( nonceStore = openid.NewSimpleNonceStore() discoveryCache = newTimedDiscoveryCache(24 * time.Hour) + + // openIDInstance does discovery/verification via an SSRF-protected client, so a user-supplied + // OpenID identifier can't reach internal/loopback/reserved addresses. It honors the operator's + // [security] ALLOWED_HOST_LIST (empty defaults to "external"), matching the avatar/webhook/migration + // clients, and validates the proxy path too. Lazy: reads proxy/settings once. + openIDInstance = sync.OnceValue(func() *openid.OpenID { + allowList := hostmatcher.ParseHostMatchList("security.ALLOWED_HOST_LIST", setting.Security.AllowedHostList) + return openid.NewOpenID(&http.Client{ + Timeout: 30 * time.Second, + Transport: hostmatcher.NewHTTPTransport("openid", allowList, nil, proxy.Proxy(), setting.Proxy.ProxyURLFixed, nil), + }) + }) ) // Verify handles response from OpenID provider func Verify(fullURL string) (id string, err error) { - return openid.Verify(fullURL, discoveryCache, nonceStore) + return openIDInstance().Verify(fullURL, discoveryCache, nonceStore) } // Normalize normalizes an OpenID URI @@ -33,5 +51,5 @@ func Normalize(url string) (id string, err error) { // RedirectURL redirects browser func RedirectURL(id, callbackURL, realm string) (string, error) { - return openid.RedirectURL(id, callbackURL, realm) + return openIDInstance().RedirectURL(id, callbackURL, realm) } diff --git a/modules/auth/openid/openid_test.go b/modules/auth/openid/openid_test.go new file mode 100644 index 0000000000..66e3e10495 --- /dev/null +++ b/modules/auth/openid/openid_test.go @@ -0,0 +1,29 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package openid + +import ( + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOpenIDDiscoveryBlocksInternalHost(t *testing.T) { + var reached atomic.Bool + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + reached.Store(true) + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + // RedirectURL performs server-side discovery of the identifier URL; a loopback URL + // must be refused at dial time instead of reaching the internal server + _, err := RedirectURL(srv.URL, "http://example.com/callback", "http://example.com/") + require.Error(t, err) + assert.False(t, reached.Load(), "OpenID discovery must not reach an internal/loopback host") +} diff --git a/modules/fileicon/entry.go b/modules/fileicon/entry.go index 2f76da2475..1174600da7 100644 --- a/modules/fileicon/entry.go +++ b/modules/fileicon/entry.go @@ -3,7 +3,11 @@ package fileicon -import "gitea.dev/modules/git" +import ( + "context" + + "gitea.dev/modules/git" +) type EntryInfo struct { BaseName string @@ -12,10 +16,10 @@ type EntryInfo struct { IsOpen bool } -func EntryInfoFromGitTreeEntry(commit *git.Commit, fullPath string, gitEntry *git.TreeEntry) *EntryInfo { +func EntryInfoFromGitTreeEntry(ctx context.Context, gitRepo *git.Repository, commit *git.Commit, fullPath string, gitEntry *git.TreeEntry) *EntryInfo { ret := &EntryInfo{BaseName: gitEntry.Name(), EntryMode: gitEntry.Mode()} if gitEntry.IsLink() { - if res, err := git.EntryFollowLink(commit, fullPath, gitEntry); err == nil && res.TargetEntry.IsDir() { + if res, err := git.EntryFollowLink(ctx, gitRepo, commit, fullPath, gitEntry); err == nil && res.TargetEntry.IsDir() { ret.SymlinkToMode = res.TargetEntry.Mode() } } diff --git a/modules/git/README.md b/modules/git/README.md deleted file mode 100644 index 4418c1b891..0000000000 --- a/modules/git/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Git Module - -This module is merged from https://github.com/go-gitea/git which is a Go module to access Git through shell commands. Now it's a part of gitea's main repository for easier pull request. diff --git a/modules/git/attribute/batch.go b/modules/git/attribute/batch.go index 96d63a910f..1e59607707 100644 --- a/modules/git/attribute/batch.go +++ b/modules/git/attribute/batch.go @@ -29,15 +29,15 @@ type BatchChecker struct { // NewBatchChecker creates a check attribute reader for the current repository and provided commit ID // If treeish is empty, then it will use current working directory, otherwise it will use the provided treeish on the bare repo -func NewBatchChecker(repo *git.Repository, treeish string, attributes []string) (checker *BatchChecker, returnedErr error) { - ctx, cancel := context.WithCancel(repo.Ctx) +func NewBatchChecker(ctx context.Context, repo *git.Repository, treeish string, attributes []string) (checker *BatchChecker, returnedErr error) { + ctx, cancel := context.WithCancel(ctx) defer func() { if returnedErr != nil { cancel() } }() - cmd, envs, cleanup, err := checkAttrCommand(repo, treeish, nil, attributes) + cmd, envs, cleanup, err := checkAttrCommand(ctx, repo, treeish, nil, attributes) if err != nil { return nil, err } diff --git a/modules/git/attribute/batch_test.go b/modules/git/attribute/batch_test.go index a71a73b302..37bb85c224 100644 --- a/modules/git/attribute/batch_test.go +++ b/modules/git/attribute/batch_test.go @@ -118,7 +118,8 @@ func expectedAttrs() *Attributes { func Test_BatchChecker(t *testing.T) { setting.AppDataPath = t.TempDir() repoPath := "../tests/repos/language_stats_repo" - gitRepo, err := git.OpenRepository(t.Context(), repoPath) + ctx := t.Context() + gitRepo, err := git.OpenRepositoryLocal(repoPath) require.NoError(t, err) defer gitRepo.Close() @@ -126,7 +127,7 @@ func Test_BatchChecker(t *testing.T) { t.Run("Create index file to run git check-attr", func(t *testing.T) { defer test.MockVariableValue(&git.DefaultFeatures().SupportCheckAttrOnBare, false)() - checker, err := NewBatchChecker(gitRepo, commitID, LinguistAttributes) + checker, err := NewBatchChecker(ctx, gitRepo, commitID, LinguistAttributes) assert.NoError(t, err) defer checker.Close() attributes, err := checker.CheckPath("i-am-a-python.p") @@ -143,11 +144,11 @@ func Test_BatchChecker(t *testing.T) { }) assert.NoError(t, err) - tempRepo, err := git.OpenRepository(t.Context(), dir) + tempRepo, err := git.OpenRepositoryLocal(dir) assert.NoError(t, err) defer tempRepo.Close() - checker, err := NewBatchChecker(tempRepo, "", LinguistAttributes) + checker, err := NewBatchChecker(t.Context(), tempRepo, "", LinguistAttributes) assert.NoError(t, err) defer checker.Close() attributes, err := checker.CheckPath("i-am-a-python.p") @@ -161,7 +162,7 @@ func Test_BatchChecker(t *testing.T) { } t.Run("Run git check-attr in bare repository", func(t *testing.T) { - checker, err := NewBatchChecker(gitRepo, commitID, LinguistAttributes) + checker, err := NewBatchChecker(ctx, gitRepo, commitID, LinguistAttributes) assert.NoError(t, err) defer checker.Close() diff --git a/modules/git/attribute/checker.go b/modules/git/attribute/checker.go index 0be610994c..f4d3e23c9c 100644 --- a/modules/git/attribute/checker.go +++ b/modules/git/attribute/checker.go @@ -14,7 +14,7 @@ import ( "gitea.dev/modules/git/gitcmd" ) -func checkAttrCommand(gitRepo *git.Repository, treeish string, filenames, attributes []string) (*gitcmd.Command, []string, func(), error) { +func checkAttrCommand(ctx context.Context, gitRepo *git.Repository, treeish string, filenames, attributes []string) (*gitcmd.Command, []string, func(), error) { cancel := func() {} envs := []string{"GIT_FLUSH=1"} cmd := gitcmd.NewCommand("check-attr", "-z") @@ -28,7 +28,7 @@ func checkAttrCommand(gitRepo *git.Repository, treeish string, filenames, attrib cmd.AddArguments("--source") cmd.AddDynamicArguments(treeish) } else { - indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(treeish) + indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(ctx, treeish) if err != nil { return nil, nil, nil, err } @@ -62,7 +62,7 @@ type CheckAttributeOpts struct { // CheckAttributes return the attributes of the given filenames and attributes in the given treeish. // If treeish is empty, then it will use current working directory, otherwise it will use the provided treeish on the bare repo func CheckAttributes(ctx context.Context, gitRepo *git.Repository, treeish string, opts CheckAttributeOpts) (map[string]*Attributes, error) { - cmd, envs, cancel, err := checkAttrCommand(gitRepo, treeish, opts.Filenames, opts.Attributes) + cmd, envs, cancel, err := checkAttrCommand(ctx, gitRepo, treeish, opts.Filenames, opts.Attributes) if err != nil { return nil, err } diff --git a/modules/git/attribute/checker_test.go b/modules/git/attribute/checker_test.go index ca10214351..83f4b79b28 100644 --- a/modules/git/attribute/checker_test.go +++ b/modules/git/attribute/checker_test.go @@ -18,7 +18,7 @@ import ( func Test_Checker(t *testing.T) { setting.AppDataPath = t.TempDir() repoPath := "../tests/repos/language_stats_repo" - gitRepo, err := git.OpenRepository(t.Context(), repoPath) + gitRepo, err := git.OpenRepositoryLocal(repoPath) require.NoError(t, err) defer gitRepo.Close() @@ -44,7 +44,7 @@ func Test_Checker(t *testing.T) { }) assert.NoError(t, err) - tempRepo, err := git.OpenRepository(t.Context(), dir) + tempRepo, err := git.OpenRepositoryLocal(dir) assert.NoError(t, err) defer tempRepo.Close() diff --git a/modules/git/blob.go b/modules/git/blob.go index eca9e37d95..0565353ef7 100644 --- a/modules/git/blob.go +++ b/modules/git/blob.go @@ -6,12 +6,12 @@ package git import ( "bytes" + "context" "encoding/base64" "errors" "io" "strings" - "gitea.dev/modules/typesniffer" "gitea.dev/modules/util" ) @@ -23,11 +23,11 @@ func (b *Blob) Name() string { } // GetBlobBytes Gets the limited content of the blob -func (b *Blob) GetBlobBytes(limit int64) ([]byte, error) { +func (b *Blob) GetBlobBytes(ctx context.Context, limit int64) ([]byte, error) { if limit <= 0 { return nil, nil } - dataRc, err := b.DataAsync() + dataRc, err := b.DataAsync(ctx) if err != nil { return nil, err } @@ -36,42 +36,43 @@ func (b *Blob) GetBlobBytes(limit int64) ([]byte, error) { } // GetBlobContent Gets the limited content of the blob as raw text -func (b *Blob) GetBlobContent(limit int64) (string, error) { - buf, err := b.GetBlobBytes(limit) +func (b *Blob) GetBlobContent(ctx context.Context, limit int64) (string, error) { + buf, err := b.GetBlobBytes(ctx, limit) return string(buf), err } // GetBlobLineCount gets line count of the blob. // It will also try to write the content to w if it's not nil, then we could pre-fetch the content without reading it again. -func (b *Blob) GetBlobLineCount(w io.Writer) (int, error) { - reader, err := b.DataAsync() +func (b *Blob) GetBlobLineCount(ctx context.Context, w io.Writer) (size int64, count int, _ error) { + reader, err := b.DataAsync(ctx) if err != nil { - return 0, err + return 0, 0, err } defer reader.Close() buf := make([]byte, 32*1024) - count := 1 + size, count = 0, 1 lineSep := []byte{'\n'} for { c, err := reader.Read(buf) + size += int64(c) if w != nil { if _, err := w.Write(buf[:c]); err != nil { - return count, err + return size, count, err } } count += bytes.Count(buf[:c], lineSep) switch { case errors.Is(err, io.EOF): - return count, nil + return size, count, nil case err != nil: - return count, err + return size, count, err } } } // GetBlobContentBase64 Reads the content of the blob with a base64 encoding and returns the encoded string -func (b *Blob) GetBlobContentBase64(originContent *strings.Builder) (string, error) { - dataRc, err := b.DataAsync() +func (b *Blob) GetBlobContentBase64(ctx context.Context, originContent *strings.Builder) (string, error) { + dataRc, err := b.DataAsync(ctx) if err != nil { return "", err } @@ -101,12 +102,3 @@ loop: _ = encoder.Close() return base64buf.String(), nil } - -// GuessContentType guesses the content type of the blob. -func (b *Blob) GuessContentType() (typesniffer.SniffedType, error) { - buf, err := b.GetBlobBytes(typesniffer.SniffContentSize) - if err != nil { - return typesniffer.SniffedType{}, err - } - return typesniffer.DetectContentType(buf), nil -} diff --git a/modules/git/blob_gogit.go b/modules/git/blob_gogit.go index 4b96c69253..e67b6c2a39 100644 --- a/modules/git/blob_gogit.go +++ b/modules/git/blob_gogit.go @@ -7,6 +7,7 @@ package git import ( + "context" "io" "gitea.dev/modules/log" @@ -27,7 +28,7 @@ func (b *Blob) gogitEncodedObj() (plumbing.EncodedObject, error) { // DataAsync gets a ReadCloser for the contents of a blob without reading it all. // Calling the Close function on the result will discard all unread output. -func (b *Blob) DataAsync() (io.ReadCloser, error) { +func (b *Blob) DataAsync(_ context.Context) (io.ReadCloser, error) { obj, err := b.gogitEncodedObj() if err != nil { return nil, err @@ -36,7 +37,7 @@ func (b *Blob) DataAsync() (io.ReadCloser, error) { } // Size returns the uncompressed size of the blob -func (b *Blob) Size() int64 { +func (b *Blob) Size(_ context.Context) int64 { obj, err := b.gogitEncodedObj() if err != nil { log.Error("Error getting gogit encoded object for blob %s(%s): %v", b.name, b.ID.String(), err) diff --git a/modules/git/blob_nogogit.go b/modules/git/blob_nogogit.go index 102faf2a5f..4803a50efe 100644 --- a/modules/git/blob_nogogit.go +++ b/modules/git/blob_nogogit.go @@ -6,6 +6,7 @@ package git import ( + "context" "io" "gitea.dev/modules/log" @@ -23,8 +24,8 @@ type Blob struct { // DataAsync gets a ReadCloser for the contents of a blob without reading it all. // Calling the Close function on the result will discard all unread output. -func (b *Blob) DataAsync() (_ io.ReadCloser, retErr error) { - batch, cancel, err := b.repo.CatFileBatch(b.repo.Ctx) +func (b *Blob) DataAsync(ctx context.Context) (_ io.ReadCloser, retErr error) { + batch, cancel, err := b.repo.CatFileBatch(ctx) if err != nil { return nil, err } @@ -50,12 +51,12 @@ func (b *Blob) DataAsync() (_ io.ReadCloser, retErr error) { } // Size returns the uncompressed size of the blob -func (b *Blob) Size() int64 { +func (b *Blob) Size(ctx context.Context) int64 { if b.gotSize { return b.size } - batch, cancel, err := b.repo.CatFileBatch(b.repo.Ctx) + batch, cancel, err := b.repo.CatFileBatch(ctx) if err != nil { log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err) return 0 diff --git a/modules/git/blob_test.go b/modules/git/blob_test.go index 4c86aa70ba..42df29bece 100644 --- a/modules/git/blob_test.go +++ b/modules/git/blob_test.go @@ -16,14 +16,14 @@ import ( func TestBlob_Data(t *testing.T) { output := "file2\n" bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - repo, err := OpenRepository(t.Context(), bareRepo1Path) + repo, err := OpenRepositoryLocal(bareRepo1Path) require.NoError(t, err) defer repo.Close() testBlob, err := repo.GetBlob("6c493ff740f9380390d5c9ddef4af18697ac9375") assert.NoError(t, err) - r, err := testBlob.DataAsync() + r, err := testBlob.DataAsync(t.Context()) assert.NoError(t, err) require.NotNil(t, r) @@ -36,7 +36,7 @@ func TestBlob_Data(t *testing.T) { func Benchmark_Blob_Data(b *testing.B) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - repo, err := OpenRepository(b.Context(), bareRepo1Path) + repo, err := OpenRepositoryLocal(bareRepo1Path) if err != nil { b.Fatal(err) } @@ -48,7 +48,7 @@ func Benchmark_Blob_Data(b *testing.B) { } for b.Loop() { - r, err := testBlob.DataAsync() + r, err := testBlob.DataAsync(b.Context()) if err != nil { b.Fatal(err) } diff --git a/modules/git/catfile_batch.go b/modules/git/catfile_batch.go index d13179f3ec..d4e697117a 100644 --- a/modules/git/catfile_batch.go +++ b/modules/git/catfile_batch.go @@ -6,6 +6,8 @@ package git import ( "context" "io" + + "gitea.dev/modules/git/gitcmd" ) type BufferedReader interface { @@ -39,12 +41,14 @@ type CatFileBatch interface { type CatFileBatchCloser interface { CatFileBatch + Context() context.Context Close() } // NewBatch creates a "batch object provider (CatFileBatch)" for the given repository path to retrieve object info and content efficiently. // The CatFileBatch and the readers create by it should only be used in the same goroutine. -func NewBatch(ctx context.Context, repoPath string) (CatFileBatchCloser, error) { +func NewBatch(ctx context.Context, repo RepositoryFacade) (CatFileBatchCloser, error) { + repoPath := gitcmd.RepoLocalPath(repo) if DefaultFeatures().SupportCatFileBatchCommand { return newCatFileBatchCommand(ctx, repoPath) } diff --git a/modules/git/catfile_batch_command.go b/modules/git/catfile_batch_command.go index 3cf7defa8b..96d58334f8 100644 --- a/modules/git/catfile_batch_command.go +++ b/modules/git/catfile_batch_command.go @@ -40,6 +40,10 @@ func (b *catFileBatchCommand) getBatch() *catFileBatchCommunicator { return b.batch } +func (b *catFileBatchCommand) Context() context.Context { + return b.ctx +} + func (b *catFileBatchCommand) QueryContent(obj string) (*CatFileObject, BufferedReader, error) { if strings.Contains(obj, "\n") { setting.PanicInDevOrTesting("invalid object name with newline: %q", obj) diff --git a/modules/git/catfile_batch_legacy.go b/modules/git/catfile_batch_legacy.go index 05c55f3008..79f12a9f43 100644 --- a/modules/git/catfile_batch_legacy.go +++ b/modules/git/catfile_batch_legacy.go @@ -51,6 +51,10 @@ func (b *catFileBatchLegacy) getBatchCheck() *catFileBatchCommunicator { return b.batchCheck } +func (b *catFileBatchLegacy) Context() context.Context { + return b.ctx +} + func (b *catFileBatchLegacy) QueryContent(obj string) (*CatFileObject, BufferedReader, error) { if strings.Contains(obj, "\n") { setting.PanicInDevOrTesting("invalid object name with newline: %q", obj) diff --git a/modules/git/catfile_batch_test.go b/modules/git/catfile_batch_test.go index 072759e409..a7902124e5 100644 --- a/modules/git/catfile_batch_test.go +++ b/modules/git/catfile_batch_test.go @@ -9,6 +9,7 @@ import ( "path/filepath" "testing" + "gitea.dev/modules/git/gitcmd" "gitea.dev/modules/test" "github.com/stretchr/testify/assert" @@ -24,9 +25,10 @@ func TestCatFileBatch(t *testing.T) { } func testCatFileBatch(t *testing.T) { + repo1 := gitcmd.RepositoryUnmanaged(filepath.Join(testReposDir, "repo1_bare")) t.Run("CorruptedGitRepo", func(t *testing.T) { tmpDir := t.TempDir() - batch, err := NewBatch(t.Context(), tmpDir) + batch, err := NewBatch(t.Context(), gitcmd.RepositoryUnmanaged(tmpDir)) // as long as the directory exists, no error, because we can't really know whether the git repo is valid until we run commands require.NoError(t, err) defer batch.Close() @@ -47,7 +49,7 @@ func testCatFileBatch(t *testing.T) { assert.ErrorIs(t, err, expectedErr) } - batch, err := NewBatch(t.Context(), filepath.Join(testReposDir, "repo1_bare")) + batch, err := NewBatch(t.Context(), repo1) require.NoError(t, err) defer batch.Close() _, err = batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449") @@ -79,7 +81,7 @@ func testCatFileBatch(t *testing.T) { simulateQueryTerminated(t, nil, os.ErrClosed) // pipes are closed faster }) - batch, err := NewBatch(t.Context(), filepath.Join(testReposDir, "repo1_bare")) + batch, err := NewBatch(t.Context(), repo1) require.NoError(t, err) defer batch.Close() diff --git a/modules/git/commit.go b/modules/git/commit.go index 9600ec1b6a..3e46987aae 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -17,17 +17,17 @@ import ( // 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" - CommitMessage ID ObjectID + TreeID ObjectID + Parents []ObjectID Author *Signature // never nil Committer *Signature // never nil Signature *CommitSignature - Parents []ObjectID // ID strings submoduleCache *ObjectCache[*SubModule] + treeCache *Tree } // CommitSignature represents a git commit signature part. @@ -46,12 +46,12 @@ func (c *Commit) ParentID(n int) (ObjectID, error) { } // Parent returns n-th parent (0-based index) of the commit. -func (c *Commit) Parent(n int) (*Commit, error) { +func (c *Commit) Parent(ctx context.Context, gitRepo *Repository, n int) (*Commit, error) { id, err := c.ParentID(n) if err != nil { return nil, err } - parent, err := c.repo.getCommit(id) + parent, err := gitRepo.getCommit(ctx, id) if err != nil { return nil, err } @@ -65,25 +65,44 @@ func (c *Commit) ParentCount() int { } // GetCommitByPath return the commit of relative path object. -func (c *Commit) GetCommitByPath(relpath string) (*Commit, error) { - if c.repo.LastCommitCache != nil { - return c.repo.LastCommitCache.GetCommitByPath(c.ID.String(), relpath) +func (c *Commit) GetCommitByPath(ctx context.Context, gitRepo *Repository, relpath string) (*Commit, error) { + if gitRepo.LastCommitCache != nil { + return gitRepo.LastCommitCache.GetCommitByPath(ctx, c.ID.String(), relpath) } - return c.repo.getCommitByPathWithID(c.ID, relpath) + return gitRepo.getCommitByPathWithID(ctx, c.ID, relpath) +} + +func (c *Commit) Tree() *Tree { + if c.treeCache == nil { + c.treeCache = newTree(c.TreeID) + } + return c.treeCache +} + +func (c *Commit) GetBlobByPath(ctx context.Context, gitRepo *Repository, relpath string) (*Blob, error) { + return c.Tree().GetBlobByPath(ctx, gitRepo, relpath) +} + +func (c *Commit) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (_ *TreeEntry, err error) { + return c.Tree().GetTreeEntryByPath(ctx, gitRepo, relpath) +} + +func (c *Commit) SubTree(ctx context.Context, gitRepo *Repository, relpath string) (*Tree, error) { + return c.Tree().SubTree(ctx, gitRepo, relpath) } // CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize -func (c *Commit) CommitsByRange(page, pageSize int, not, since, until string) ([]*Commit, error) { - return c.repo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until) +func (c *Commit) CommitsByRange(ctx context.Context, gitRepo *Repository, page, pageSize int, not, since, until string) ([]*Commit, error) { + return gitRepo.commitsByRangeWithTime(ctx, c.ID, page, pageSize, not, since, until) } // CommitsBefore returns all the commits before current revision -func (c *Commit) CommitsBefore() ([]*Commit, error) { - return c.repo.getCommitsBefore(c.ID) +func (c *Commit) CommitsBefore(ctx context.Context, gitRepo *Repository) ([]*Commit, error) { + return gitRepo.getCommitsBefore(ctx, c.ID) } // HasPreviousCommit returns true if a given commitHash is contained in commit's parents -func (c *Commit) HasPreviousCommit(objectID ObjectID) (bool, error) { +func (c *Commit) HasPreviousCommit(ctx context.Context, gitRepo *Repository, objectID ObjectID) (bool, error) { this := c.ID.String() that := objectID.String() @@ -93,8 +112,8 @@ func (c *Commit) HasPreviousCommit(objectID ObjectID) (bool, error) { _, _, err := gitcmd.NewCommand("merge-base", "--is-ancestor"). AddDynamicArguments(that, this). - WithDir(c.repo.Path). - RunStdString(c.repo.Ctx) + WithDir(gitRepo.Path). + RunStdString(ctx) if err == nil { return true, nil } @@ -108,8 +127,8 @@ func (c *Commit) HasPreviousCommit(objectID ObjectID) (bool, error) { } // IsForcePush returns true if a push from oldCommitHash to this is a force push -func (c *Commit) IsForcePush(oldCommitID string) (bool, error) { - objectFormat, err := c.repo.GetObjectFormat() +func (c *Commit) IsForcePush(ctx context.Context, gitRepo *Repository, oldCommitID string) (bool, error) { + objectFormat, err := gitRepo.GetObjectFormat(ctx) if err != nil { return false, err } @@ -117,22 +136,22 @@ func (c *Commit) IsForcePush(oldCommitID string) (bool, error) { return false, nil } - oldCommit, err := c.repo.GetCommit(oldCommitID) + oldCommit, err := gitRepo.GetCommit(ctx, oldCommitID) if err != nil { return false, err } - hasPreviousCommit, err := c.HasPreviousCommit(oldCommit.ID) + hasPreviousCommit, err := c.HasPreviousCommit(ctx, gitRepo, oldCommit.ID) return !hasPreviousCommit, err } // CommitsBeforeLimit returns num commits before current revision -func (c *Commit) CommitsBeforeLimit(num int) ([]*Commit, error) { - return c.repo.getCommitsBeforeLimit(c.ID, num) +func (c *Commit) CommitsBeforeLimit(ctx context.Context, gitRepo *Repository, num int) ([]*Commit, error) { + return gitRepo.getCommitsBeforeLimit(ctx, c.ID, num) } // CommitsBeforeUntil returns the commits in range "[cur, ref)" -func (c *Commit) CommitsBeforeUntil(ref RefName) ([]*Commit, error) { - return c.repo.CommitsBetween(c.ID.RefName(), ref, -1) +func (c *Commit) CommitsBeforeUntil(ctx context.Context, gitRepo *Repository, ref RefName) ([]*Commit, error) { + return gitRepo.CommitsBetween(ctx, c.ID.RefName(), ref, -1) } // SearchCommitsOptions specify the parameters for SearchCommits @@ -175,39 +194,29 @@ func NewSearchCommitsOptions(searchString string, forAllRefs bool) SearchCommits } // SearchCommits returns the commits match the keyword before current revision -func (c *Commit) SearchCommits(opts SearchCommitsOptions) ([]*Commit, error) { - return c.repo.searchCommits(c.ID, opts) +func (c *Commit) SearchCommits(ctx context.Context, gitRepo *Repository, opts SearchCommitsOptions) ([]*Commit, error) { + return gitRepo.searchCommits(ctx, c.ID, opts) } // GetFilesChangedSinceCommit get all changed file names between pastCommit to current revision -func (c *Commit) GetFilesChangedSinceCommit(pastCommit string) ([]string, error) { - return c.repo.GetFilesChangedBetween(pastCommit, c.ID.String()) +func (c *Commit) GetFilesChangedSinceCommit(ctx context.Context, gitRepo *Repository, pastCommit string) ([]string, error) { + return gitRepo.GetFilesChangedBetween(ctx, pastCommit, c.ID.String()) } // FileChangedSinceCommit Returns true if the file given has changed since the past commit // YOU MUST ENSURE THAT pastCommit is a valid commit ID. -func (c *Commit) FileChangedSinceCommit(filename, pastCommit string) (bool, error) { - return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String()) -} - -// HasFile returns true if the file given exists on this commit -// This does only mean it's there - it does not mean the file was changed during the commit. -func (c *Commit) HasFile(filename string) (bool, error) { - _, err := c.GetBlobByPath(filename) - if err != nil { - return false, err - } - return true, nil +func (c *Commit) FileChangedSinceCommit(ctx context.Context, gitRepo *Repository, filename, pastCommit string) (bool, error) { + return gitRepo.FileChangedBetweenCommits(ctx, filename, pastCommit, c.ID.String()) } // GetFileContent reads a file content as a string or returns false if this was not possible -func (c *Commit) GetFileContent(filename string, limit int) (string, error) { - entry, err := c.GetTreeEntryByPath(filename) +func (c *Commit) GetFileContent(ctx context.Context, gitRepo *Repository, filename string, limit int) (string, error) { + entry, err := c.GetTreeEntryByPath(ctx, gitRepo, filename) if err != nil { return "", err } - r, err := entry.Blob().DataAsync() + r, err := entry.Blob(gitRepo).DataAsync(ctx) if err != nil { return "", err } diff --git a/modules/git/commit_info.go b/modules/git/commit_info.go index 4f76a28f31..87a6ddd8d1 100644 --- a/modules/git/commit_info.go +++ b/modules/git/commit_info.go @@ -3,6 +3,8 @@ package git +import "context" + // CommitInfo describes the first commit with the provided entry type CommitInfo struct { Entry *TreeEntry @@ -10,8 +12,8 @@ type CommitInfo struct { SubmoduleFile *CommitSubmoduleFile } -func GetCommitInfoSubmoduleFile(repoLink, fullPath string, commit *Commit, refCommitID ObjectID) (*CommitSubmoduleFile, error) { - submodule, err := commit.GetSubModule(fullPath) +func GetCommitInfoSubmoduleFile(ctx context.Context, repoLink, fullPath string, gitRepo *Repository, commit *Commit, refCommitID ObjectID) (*CommitSubmoduleFile, error) { + submodule, err := commit.GetSubModule(ctx, gitRepo, fullPath) if err != nil { return nil, err } diff --git a/modules/git/commit_info_gogit.go b/modules/git/commit_info_gogit.go index 6174f46f07..f5c01ad2cc 100644 --- a/modules/git/commit_info_gogit.go +++ b/modules/git/commit_info_gogit.go @@ -17,7 +17,7 @@ import ( ) // GetCommitsInfo gets information of all commits that are corresponding to these entries -func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) { +func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, gitRepo *Repository, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) { entryPaths := make([]string, len(tes)+1) // Get the commit for the treePath itself entryPaths[0] = "" @@ -25,25 +25,16 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit * entryPaths[i+1] = entry.Name() } - commitNodeIndex, commitGraphFile := commit.repo.CommitNodeIndex() - if commitGraphFile != nil { - defer commitGraphFile.Close() - } - - c, err := commitNodeIndex.Get(plumbing.Hash(commit.ID.RawValue())) - if err != nil { - return nil, nil, err - } - var revs map[string]*Commit - if commit.repo.LastCommitCache != nil { + var err error + if gitRepo.LastCommitCache != nil { var unHitPaths []string - revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, commit.repo.LastCommitCache) + revs, unHitPaths, err = getLastCommitForPathsByCache(ctx, commit.ID.String(), treePath, entryPaths, gitRepo.LastCommitCache) if err != nil { return nil, nil, err } if len(unHitPaths) > 0 { - revs2, err := GetLastCommitForPaths(ctx, commit.repo.LastCommitCache, c, treePath, unHitPaths) + revs2, err := GetLastCommitForPaths(ctx, gitRepo, commit, treePath, unHitPaths) if err != nil { return nil, nil, err } @@ -51,13 +42,13 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit * maps.Copy(revs, revs2) } } else { - revs, err = GetLastCommitForPaths(ctx, nil, c, treePath, entryPaths) + revs, err = GetLastCommitForPaths(ctx, gitRepo, commit, treePath, entryPaths) } if err != nil { return nil, nil, err } - commit.repo.gogitStorage.Close() + gitRepo.gogitStorage.Close() commitsInfo := make([]CommitInfo, len(tes)) for i, entry := range tes { @@ -72,7 +63,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit * // If the entry is a submodule, add a submodule file for this if entry.IsSubModule() { - commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(repoLink, path.Join(treePath, entry.Name()), commit, entry.ID) + commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(ctx, repoLink, path.Join(treePath, entry.Name()), gitRepo, commit, entry.ID) if err != nil { return nil, nil, err } @@ -83,11 +74,10 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit * // get it for free during the tree traversal and it's used for listing // pages to display information about newest commit for a given path. var treeCommit *Commit - var ok bool if treePath == "" { treeCommit = commit - } else if treeCommit, ok = revs[""]; ok { - treeCommit.repo = commit.repo + } else { + treeCommit = revs[""] } return commitsInfo, treeCommit, nil } @@ -142,11 +132,11 @@ func getFileHashes(c cgobject.CommitNode, treePath string, paths []string) (map[ return hashes, nil } -func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) { +func getLastCommitForPathsByCache(ctx context.Context, commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) { var unHitEntryPaths []string results := make(map[string]*Commit) for _, p := range paths { - lastCommit, err := cache.Get(commitID, path.Join(treePath, p)) + lastCommit, err := cache.Get(ctx, commitID, path.Join(treePath, p)) if err != nil { return nil, nil, err } @@ -162,7 +152,20 @@ func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cac } // GetLastCommitForPaths returns last commit information -func GetLastCommitForPaths(ctx context.Context, cache *LastCommitCache, c cgobject.CommitNode, treePath string, paths []string) (map[string]*Commit, error) { +func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) { + commitNodeIndex, commitGraphFile := gitRepo.CommitNodeIndex() + if commitGraphFile != nil { + defer commitGraphFile.Close() + } + + c, err := commitNodeIndex.Get(plumbing.Hash(commit.ID.RawValue())) + if err != nil { + return nil, err + } + return getLastCommitForPathsByCommitNode(ctx, gitRepo, c, treePath, paths) +} + +func getLastCommitForPathsByCommitNode(ctx context.Context, gitRepo *Repository, c cgobject.CommitNode, treePath string, paths []string) (map[string]*Commit, error) { refSha := c.ID().String() // We do a tree traversal with nodes sorted by commit time @@ -243,7 +246,7 @@ heaploop: // match any of the hashes being merged. This is more common for directories, // but it can also happen if a file is changed through conflict resolution. resultNodes[pth] = current.commit - if err := cache.Put(refSha, path.Join(treePath, pth), current.commit.ID().String()); err != nil { + if err := gitRepo.LastCommitCache.Put(refSha, path.Join(treePath, pth), current.commit.ID().String()); err != nil { return nil, err } } diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go index cf53f9fdf4..2475388273 100644 --- a/modules/git/commit_info_nogogit.go +++ b/modules/git/commit_info_nogogit.go @@ -15,7 +15,7 @@ import ( ) // GetCommitsInfo gets information of all commits that are corresponding to these entries -func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) { +func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, gitRepo *Repository, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) { entryPaths := make([]string, len(tes)+1) // Get the commit for the treePath itself entryPaths[0] = "" @@ -26,15 +26,15 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit * var err error var revs map[string]*Commit - if commit.repo.LastCommitCache != nil { + if gitRepo.LastCommitCache != nil { var unHitPaths []string - revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, commit.repo.LastCommitCache) + revs, unHitPaths, err = getLastCommitForPathsByCache(ctx, commit.ID.String(), treePath, entryPaths, gitRepo.LastCommitCache) if err != nil { return nil, nil, err } if len(unHitPaths) > 0 { sort.Strings(unHitPaths) - commits, err := GetLastCommitForPaths(ctx, commit, treePath, unHitPaths) + commits, err := GetLastCommitForPaths(ctx, gitRepo, commit, treePath, unHitPaths) if err != nil { return nil, nil, err } @@ -43,7 +43,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit * } } else { sort.Strings(entryPaths) - revs, err = GetLastCommitForPaths(ctx, commit, treePath, entryPaths) + revs, err = GetLastCommitForPaths(ctx, gitRepo, commit, treePath, entryPaths) } if err != nil { return nil, nil, err @@ -64,7 +64,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit * // If the entry is a submodule, add a submodule file for this if entry.IsSubModule() { - commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(repoLink, path.Join(treePath, entry.Name()), commit, entry.ID) + commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(ctx, repoLink, path.Join(treePath, entry.Name()), gitRepo, commit, entry.ID) if err != nil { return nil, nil, err } @@ -75,20 +75,19 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit * // get it for free during the tree traversal, and it's used for listing // pages to display information about the newest commit for a given path. var treeCommit *Commit - var ok bool if treePath == "" { treeCommit = commit - } else if treeCommit, ok = revs[""]; ok { - treeCommit.repo = commit.repo + } else { + treeCommit = revs[""] } return commitsInfo, treeCommit, nil } -func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) { +func getLastCommitForPathsByCache(ctx context.Context, commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) { var unHitEntryPaths []string results := make(map[string]*Commit) for _, p := range paths { - lastCommit, err := cache.Get(commitID, path.Join(treePath, p)) + lastCommit, err := cache.Get(ctx, commitID, path.Join(treePath, p)) if err != nil { return nil, nil, err } @@ -104,9 +103,9 @@ func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cac } // GetLastCommitForPaths returns last commit information -func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) { +func GetLastCommitForPaths(ctx context.Context, gitRepo *Repository, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) { // We read backwards from the commit to obtain all of the commits - revs, err := WalkGitLog(ctx, commit.repo, commit, treePath, paths...) + revs, err := walkGitLog(ctx, gitRepo, commit, treePath, paths...) if err != nil { return nil, err } @@ -126,7 +125,7 @@ func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string, continue } - c, err := commit.repo.GetCommit(commitID) // Ensure the commit exists in the repository + c, err := gitRepo.GetCommit(ctx, commitID) // Ensure the commit exists in the repository if err != nil { return nil, err } diff --git a/modules/git/commit_info_nogogit_test.go b/modules/git/commit_info_nogogit_test.go new file mode 100644 index 0000000000..f1f9bfcb1e --- /dev/null +++ b/modules/git/commit_info_nogogit_test.go @@ -0,0 +1,54 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build !gogit + +package git + +import ( + "context" + "path/filepath" + "testing" + + "gitea.dev/modules/test" + "gitea.dev/modules/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEntries_GetCommitsInfo_ContextErr(t *testing.T) { + repo, err := OpenRepositoryLocal(filepath.Join(testReposDir, "repo1_bare")) + require.NoError(t, err) + defer repo.Close() + + commit, err := repo.GetCommit(t.Context(), "feaf4ba6bc635fec442f46ddd4512416ec43c2c2") + require.NoError(t, err) + entries, err := commit.Tree().ListEntries(t.Context(), repo) + require.NoError(t, err) + + countCommitInfosCommit := func(infos []CommitInfo) (nilCommits, nonNilCommits int) { + for _, info := range infos { + nilCommits += util.Iif(info.Commit == nil, 1, 0) + nonNilCommits += util.Iif(info.Commit != nil, 1, 0) + } + return nilCommits, nonNilCommits + } + + ctx, cancel := context.WithCancel(t.Context()) + defer test.MockVariableValue(&walkGitLogDebugBeforeNext)() + + walkGitLogDebugBeforeNext = cancel + commitInfos, _, err := entries.GetCommitsInfo(ctx, "/any/repo-link", repo, commit, "") + assert.NoError(t, err) + nilCommits, nonNilCommits := countCommitInfosCommit(commitInfos) + assert.Equal(t, 0, nonNilCommits) // no commit info due to canceled (or deadline-exceeded) context + assert.Equal(t, 3, nilCommits) + + walkGitLogDebugBeforeNext = nil + commitInfos, _, err = entries.GetCommitsInfo(t.Context(), "/any/repo-link", repo, commit, "") + assert.NoError(t, err) + nilCommits, nonNilCommits = countCommitInfosCommit(commitInfos) + assert.Equal(t, 3, nonNilCommits) + assert.Equal(t, 0, nilCommits) +} diff --git a/modules/git/commit_info_test.go b/modules/git/commit_info_test.go index 1e1697b006..ceed96867f 100644 --- a/modules/git/commit_info_test.go +++ b/modules/git/commit_info_test.go @@ -12,10 +12,6 @@ import ( "github.com/stretchr/testify/require" ) -const ( - testReposDir = "tests/repos/" -) - func cloneRepo(tb testing.TB, url string) (string, error) { repoDir := tb.TempDir() if err := Clone(tb.Context(), url, repoDir, CloneRepoOptions{ @@ -84,17 +80,16 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) { }, "feaf4ba6bc635fec442f46ddd4512416ec43c2c2"}, } for _, testCase := range testCases { - commit, err := repo1.GetCommit(testCase.CommitID) + commit, err := repo1.GetCommit(t.Context(), testCase.CommitID) if err != nil { assert.NoError(t, err, "Unable to get commit: %s from testcase due to error: %v", testCase.CommitID, err) // no point trying to do anything else for this test. continue } assert.NotNil(t, commit) - assert.NotNil(t, commit.Tree) - assert.NotNil(t, commit.Tree.repo) + assert.NotNil(t, commit.TreeID) - tree, err := commit.Tree.SubTree(testCase.Path) + tree, err := commit.SubTree(t.Context(), repo1, testCase.Path) if err != nil { assert.NoError(t, err, "Unable to get subtree: %s of commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err) // no point trying to do anything else for this test. @@ -102,9 +97,8 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) { } assert.NotNil(t, tree, "tree is nil for testCase CommitID %s in Path %s", testCase.CommitID, testCase.Path) - assert.NotNil(t, tree.repo, "repo is nil for testCase CommitID %s in Path %s", testCase.CommitID, testCase.Path) - entries, err := tree.ListEntries() + entries, err := tree.ListEntries(t.Context(), repo1) if err != nil { assert.NoError(t, err, "Unable to get entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err) // no point trying to do anything else for this test. @@ -112,7 +106,7 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) { } // FIXME: Context.TODO() - if graceful has started we should use its Shutdown context otherwise use install signals in TestMain. - commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), "/any/repo-link", commit, testCase.Path) + commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), "/any/repo-link", repo1, commit, testCase.Path) assert.NoError(t, err, "Unable to get commit information for entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err) if err != nil { t.FailNow() @@ -127,14 +121,14 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) { continue } assert.Equal(t, expectedInfo.CommitID, commit.ID.String()) - assert.Equal(t, expectedInfo.Size, entry.Size(), entry.Name()) + assert.Equal(t, expectedInfo.Size, entry.GetSize(t.Context(), repo1), entry.Name()) } } } func TestEntries_GetCommitsInfo(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) + bareRepo1, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer bareRepo1.Close() @@ -144,7 +138,7 @@ func TestEntries_GetCommitsInfo(t *testing.T) { if err != nil { assert.NoError(t, err) } - clonedRepo1, err := OpenRepository(t.Context(), clonedPath) + clonedRepo1, err := OpenRepositoryLocal(clonedPath) if err != nil { assert.NoError(t, err) } @@ -153,11 +147,11 @@ func TestEntries_GetCommitsInfo(t *testing.T) { testGetCommitsInfo(t, clonedRepo1) t.Run("NonExistingSubmoduleAsNil", func(t *testing.T) { - commit, err := bareRepo1.GetCommit("HEAD") + commit, err := bareRepo1.GetCommit(t.Context(), "HEAD") require.NoError(t, err) - treeEntry, err := commit.GetTreeEntryByPath("file1.txt") + treeEntry, err := commit.GetTreeEntryByPath(t.Context(), bareRepo1, "file1.txt") require.NoError(t, err) - cisf, err := GetCommitInfoSubmoduleFile("/any/repo-link", "file1.txt", commit, treeEntry.ID) + cisf, err := GetCommitInfoSubmoduleFile(t.Context(), "/any/repo-link", "file1.txt", bareRepo1, commit, treeEntry.ID) require.NoError(t, err) assert.Equal(t, &CommitSubmoduleFile{ repoLink: "/any/repo-link", @@ -169,52 +163,3 @@ func TestEntries_GetCommitsInfo(t *testing.T) { assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context())) }) } - -func BenchmarkEntries_GetCommitsInfo(b *testing.B) { - type benchmarkType struct { - url string - name string - } - - benchmarks := []benchmarkType{ - {url: "https://github.com/go-gitea/gitea.git", name: "gitea"}, - {url: "https://github.com/ethantkoenig/manyfiles.git", name: "manyfiles"}, - {url: "https://github.com/moby/moby.git", name: "moby"}, - {url: "https://github.com/golang/go.git", name: "go"}, - {url: "https://github.com/torvalds/linux.git", name: "linux"}, - } - - doBenchmark := func(benchmark benchmarkType) { - var commit *Commit - var entries Entries - var repo *Repository - repoPath, err := cloneRepo(b, benchmark.url) - if err != nil { - b.Fatal(err) - } - - if repo, err = OpenRepository(b.Context(), repoPath); err != nil { - b.Fatal(err) - } - defer repo.Close() - - if commit, err = repo.GetBranchCommit("master"); err != nil { - b.Fatal(err) - } else if entries, err = commit.Tree.ListEntries(); err != nil { - b.Fatal(err) - } - b.ResetTimer() - b.Run(benchmark.name, func(b *testing.B) { - for b.Loop() { - _, _, err := entries.GetCommitsInfo(b.Context(), "/any/repo-link", commit, "") - if err != nil { - b.Fatal(err) - } - } - }) - } - - for _, benchmark := range benchmarks { - doBenchmark(benchmark) - } -} diff --git a/modules/git/commit_message.go b/modules/git/commit_message.go index 0729609d87..14c7b8f7e2 100644 --- a/modules/git/commit_message.go +++ b/modules/git/commit_message.go @@ -10,16 +10,22 @@ import ( "sync" "gitea.dev/modules/charset" - "gitea.dev/modules/container" "gitea.dev/modules/util" ) // CoAuthoredByTrailer is the canonical token for the `Co-authored-by:` git trailer. const CoAuthoredByTrailer = "Co-authored-by" +const ( + commitIdentityRoleAuthor = 1 + commitIdentityRoleCommitter = 2 + commitIdentityRoleCoAuthor = 3 +) + type CommitIdentity struct { Name string Email string + role int } // CommitMessageTrailerValues keys are all in lower-case @@ -33,7 +39,9 @@ type CommitMessage struct { trailerValues CommitMessageTrailerValues - allParticipants []*CommitIdentity + allParticipants []*CommitIdentity + committerCoAuthorIdx int + committerCoAuthor *CommitIdentity } func (c *CommitMessage) MessageUTF8() string { @@ -69,8 +77,12 @@ func (c *CommitMessage) MessageTrailer() CommitMessageTrailerValues { } var commitMessageTrailerSplit = sync.OnceValue(func() *regexp.Regexp { - // the sep is either something like "\n---\n" or "\n\n" in the body, or at the start of the body like "---\n" - return regexp.MustCompile(`(?s)^(?P.*?)(?P^|^\n|^-{3,}\n+|\n-{3,}\n+|\n\n)(?P(?:[A-Za-z0-9][-A-Za-z0-9]*:[^\n]*\n?)*\n*)$`) + // ref: https://git-scm.com/docs/git-interpret-trailers + // TODO: the regexp is not able to perfectly parse the all kinds of trailers + // It was just copied from legacy code, it is not exactly the same as how Git parses the trailer and not quite right in some cases. + // For the key characters: it follows RFC 822 field name syntax (or RFC 2822/RFC 5322): printable ASCII characters between 33 and 126 except the colon (:), + // but maybe we don't want to make it that complicated, so here we only support some common "symbol-like" characters. + return regexp.MustCompile(`(?s)^(?P.*?)(?P^|^\n|^-{3,}\n+|\n+-{3,}\n+|\n{2,})(?P(?:[A-Za-z0-9][-\w]*:[^\n]*(\n\s+[^\n]*)*\n?)*\n*)$`) }) // CommitMessageSplitTrailer tries to split the message by the trailer separator @@ -85,6 +97,41 @@ func CommitMessageSplitTrailer(s string) (content, sep, trailer string) { return v[re.SubexpIndex("content")], v[re.SubexpIndex("sep")], v[re.SubexpIndex("trailer")] } +// CommitMessageMerge merges two commit messages with their trailers +func CommitMessageMerge(m1, m2 string) string { + c1, s1, t1 := CommitMessageSplitTrailer(m1) + c2, s2, t2 := CommitMessageSplitTrailer(m2) + c1, t1 = strings.TrimSpace(c1), strings.TrimSpace(t1) + c2, t2 = strings.TrimSpace(c2), strings.TrimSpace(t2) + out := strings.Builder{} + if c1 != "" && c2 != "" { + out.WriteString(c1) + out.WriteString("\n\n") + out.WriteString(c2) + } else if c1 != "" { + out.WriteString(c1) + } else if c2 != "" { + out.WriteString(c2) + } + if t1 != "" || t2 != "" { + sep := util.Iif(t1 == "", s2, s1) + sep = util.IfZero(sep, "\n\n") + if c1 != "" || c2 != "" { + out.WriteString(sep) + } + if t1 != "" { + out.WriteString(t1) + } + if t1 != "" && t2 != "" { + out.WriteString("\n") + } + if t2 != "" { + out.WriteString(t2) + } + } + return out.String() +} + func CommitMessageParseTrailer(s string) CommitMessageTrailerValues { ret := CommitMessageTrailerValues{} for line := range strings.SplitSeq(util.NormalizeStringEOL(s), "\n") { @@ -105,29 +152,57 @@ func (c *Commit) AllParticipantIdentities() []*CommitIdentity { return c.allParticipants } - exclude := container.Set[string]{} - c.allParticipants = append(c.allParticipants, &CommitIdentity{Name: c.Author.Name, Email: c.Author.Email}) - exclude.Add(strings.ToLower(c.Author.Email)) - - addParticipant := func(name, email string) { + exclude := map[string]int{} + addParticipant := func(name, email string, role int) (existingRole int) { if name == "" && email == "" { - return + return 0 } emailLower := strings.ToLower(email) - if emailLower != "" && exclude.Contains(emailLower) { - return + if existingRole = exclude[emailLower]; emailLower != "" && existingRole != 0 { + return existingRole } - c.allParticipants = append(c.allParticipants, &CommitIdentity{Name: name, Email: email}) - exclude.Add(emailLower) + c.allParticipants = append(c.allParticipants, &CommitIdentity{Name: name, Email: email, role: role}) + exclude[emailLower] = role + return 0 } - addParticipant(c.Committer.Name, c.Committer.Email) + + c.committerCoAuthorIdx = -1 + addParticipant(c.Author.Name, c.Author.Email, commitIdentityRoleAuthor) + addParticipant(c.Committer.Name, c.Committer.Email, commitIdentityRoleCommitter) for _, coAuthorValue := range c.MessageTrailer()["co-authored-by"] { addr, err := mail.ParseAddress(coAuthorValue) + coAuthorName, coAuthorEmail := coAuthorValue, "" if err == nil { - addParticipant(addr.Name, addr.Address) - } else { - addParticipant(coAuthorValue, "") + coAuthorName, coAuthorEmail = addr.Name, addr.Address + } + existingRole := addParticipant(coAuthorName, coAuthorEmail, commitIdentityRoleCoAuthor) + if existingRole == commitIdentityRoleCommitter && c.committerCoAuthorIdx == -1 { + c.committerCoAuthorIdx = len(c.allParticipants) + c.committerCoAuthor = &CommitIdentity{coAuthorName, coAuthorEmail, commitIdentityRoleCoAuthor} } } return c.allParticipants } + +// CoAuthorIdentities returns co-author identities defined by "Co-authored-by:" in the git message trailer +// Only the commit's author is excluded. If committer is declared as co-author, it will be included in the result. +// * Author & Co-author: they changed the code (attribution) +// * Committer: they submitted the commit but didn't change the code (e.g.: maintainer signed a commit) +// So, a committer can also be a co-author if they changed the code. +func (c *Commit) CoAuthorIdentities() (coAuthors []*CommitIdentity) { + all := c.AllParticipantIdentities() + if len(all) <= 1 { + return nil // no co-author list + } + if all[1].role != commitIdentityRoleCommitter { + return all[1:] // no committer, so all after author are co-authors + } + if c.committerCoAuthorIdx == -1 { + return all[2:] // the committer is not in the co-author list, so just return the co-author list + } + // the committer is in the co-author list but de-duplicated, so include them as co-author again + coAuthors = append(coAuthors, all[2:c.committerCoAuthorIdx]...) + coAuthors = append(coAuthors, c.committerCoAuthor) + coAuthors = append(coAuthors, all[c.committerCoAuthorIdx:]...) + return coAuthors +} diff --git a/modules/git/commit_message_test.go b/modules/git/commit_message_test.go index 1d13f81803..a602185d56 100644 --- a/modules/git/commit_message_test.go +++ b/modules/git/commit_message_test.go @@ -26,10 +26,12 @@ func TestCommitMessageTrailer(t *testing.T) { {"a", "a", "", ""}, {"a\n\nk", "a\n\nk", "", ""}, {"a\n\nk:v", "a", "\n\n", "k:v"}, + {"a\n\nk:v\n next-line", "a", "\n\n", "k:v\n next-line"}, + {"a\n\nk:v\n next-line\nother: v", "a", "\n\n", "k:v\n next-line\nother: v"}, {"a\n\nk:v\n\n", "a", "\n\n", "k:v\n\n"}, {"a\n--\nk:v", "a\n--\nk:v", "", ""}, - {"a\n---\nk:v", "a", "\n---\n", "k:v"}, - {"a\n\n---\n\nk:v", "a\n", "\n---\n\n", "k:v"}, + {"a\n---\nk:v", "a", "\n---\n", "k:v"}, // TODO: should we support such case? No empty line between "---" and the trailer + {"a\n\n---\n\nk:v", "a", "\n\n---\n\n", "k:v"}, {"k: v", "", "", "k: v"}, {"\nk:v", "", "\n", "k:v"}, @@ -47,36 +49,111 @@ func TestCommitMessageTrailer(t *testing.T) { } } -func TestCommitMessageAllParticipantIdentities(t *testing.T) { +func TestCommitMessageParticipants(t *testing.T) { sig := func(n, e string) *Signature { return &Signature{Name: n, Email: e} } - idt := func(n, e string) *CommitIdentity { return &CommitIdentity{Name: n, Email: e} } - cases := []struct { - commit *Commit - participant []*CommitIdentity - }{ - { - &Commit{ - Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"), - CommitMessage: CommitMessage{MessageRaw: "CO-Authored-BY: x@m.com"}, - }, - []*CommitIdentity{idt("a", "a@m.com"), idt("c", "c@m.com"), idt("", "x@m.com")}, - }, - { - &Commit{ - Author: sig("a", "a@m.com"), Committer: sig("a", "A@M.com"), - CommitMessage: CommitMessage{MessageRaw: "CO-Authored-BY: a@m.com"}, - }, - []*CommitIdentity{idt("a", "a@m.com")}, - }, - { - &Commit{ - Author: sig("a", "a@m.com"), Committer: sig("", ""), - CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: Full Name "}, - }, - []*CommitIdentity{idt("a", "a@m.com"), idt("Full Name", "X@M.com")}, - }, + idt := func(n, e string, r int) *CommitIdentity { return &CommitIdentity{n, e, r} } + roleAuthor, roleCommitter, roleCoAuthor := commitIdentityRoleAuthor, commitIdentityRoleCommitter, commitIdentityRoleCoAuthor + type testCase struct { + name string + commit *Commit + identities []*CommitIdentity } - for _, c := range cases { - assert.Equal(t, c.participant, c.commit.AllParticipantIdentities()) + t.Run("AllParticipants", func(t *testing.T) { + cases := []testCase{ + { + "DifferentUsers", + &Commit{ + Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"), + CommitMessage: CommitMessage{MessageRaw: "CO-Authored-BY: x@m.com"}, + }, + []*CommitIdentity{idt("a", "a@m.com", roleAuthor), idt("c", "c@m.com", roleCommitter), idt("", "x@m.com", roleCoAuthor)}, + }, + { + "SameUser", + &Commit{ + Author: sig("a", "a@m.com"), Committer: sig("a", "A@M.com"), + CommitMessage: CommitMessage{MessageRaw: "CO-Authored-BY: a@m.com"}, + }, + []*CommitIdentity{idt("a", "a@m.com", roleAuthor)}, + }, + { + "NoCommitter", + &Commit{ + Author: sig("a", "a@m.com"), Committer: sig("", ""), + CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: Full Name "}, + }, + []*CommitIdentity{idt("a", "a@m.com", roleAuthor), idt("Full Name", "X@M.com", roleCoAuthor)}, + }, + } + for _, c := range cases { + assert.Equal(t, c.identities, c.commit.AllParticipantIdentities(), "case: %s", c.name) + } + }) + t.Run("CoAuthors", func(t *testing.T) { + cases := []testCase{ + { + "GenuineCoAuthor", + &Commit{ + Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"), + CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: x "}, + }, + []*CommitIdentity{idt("x", "x@m.com", roleCoAuthor)}, + }, + { + "CoAuthorIsCommitter", + &Commit{ + Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"), + CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: c "}, + }, + []*CommitIdentity{idt("c", "c@m.com", roleCoAuthor)}, + }, + { + "CoAuthorIsAuthor", + &Commit{ + Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"), + CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: a "}, + }, + []*CommitIdentity{}, + }, + { + "CoAuthorCommitterNameWithIndex", // restore the committer co-author to the co-author list by the index with correct name + &Commit{ + Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"), + CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: x \nCo-authored-by: c-other \nCo-authored-by: y "}, + }, + []*CommitIdentity{idt("x", "x@m.com", roleCoAuthor), idt("c-other", "c@m.com", roleCoAuthor), idt("y", "y@m.com", roleCoAuthor)}, + }, + } + for _, c := range cases { + assert.Equal(t, c.identities, c.commit.CoAuthorIdentities(), "case: %s", c.name) + } + }) +} + +func TestCommitMessageMerge(t *testing.T) { + cases := []struct { + m1, m2 string + out string + }{ + {"", "", ""}, + {"msg1", "", "msg1"}, + {"", "msg2", "msg2"}, + {"msg1", "msg2", "msg1\n\nmsg2"}, + {"k1: a", "", "k1: a"}, + {"", "k2: b", "k2: b"}, + {"k1: a", "k2: b", "k1: a\nk2: b"}, + {"msg1", "k2: b", "msg1\n\nk2: b"}, + {"k1: a", "msg2", "msg2\n\nk1: a"}, + {"msg1\n\nk1: a", "msg2", "msg1\n\nmsg2\n\nk1: a"}, + {"msg1\n----\nk1: a", "msg2", "msg1\n\nmsg2\n----\nk1: a"}, + {"msg1\n\n----\n\nk1: a", "msg2", "msg1\n\nmsg2\n\n----\n\nk1: a"}, + {"msg1", "msg2\n----\nk2: b", "msg1\n\nmsg2\n----\nk2: b"}, + {"msg1", "msg2\n\nk2: b", "msg1\n\nmsg2\n\nk2: b"}, + {"msg1\n\nk1: a", "msg2\n\nk2: b", "msg1\n\nmsg2\n\nk1: a\nk2: b"}, + } + + for i, c := range cases { + out := CommitMessageMerge(c.m1, c.m2) + assert.Equal(t, c.out, out, "idx=%d, m1=%q m2=%q", i, c.m1, c.m2) } } diff --git a/modules/git/commit_reader.go b/modules/git/commit_reader.go index ce48d6f319..350f93c74a 100644 --- a/modules/git/commit_reader.go +++ b/modules/git/commit_reader.go @@ -15,7 +15,7 @@ const ( commitHeaderGpgsigSha256 = "gpgsig-sha256" ) -func assignCommitFields(gitRepo *Repository, commit *Commit, headerKey string, headerValue []byte) error { +func assignCommitFields(commit *Commit, headerKey string, headerValue []byte) error { if len(headerValue) > 0 && headerValue[len(headerValue)-1] == '\n' { headerValue = headerValue[:len(headerValue)-1] // remove trailing newline } @@ -25,7 +25,7 @@ func assignCommitFields(gitRepo *Repository, commit *Commit, headerKey string, h if err != nil { return fmt.Errorf("invalid tree ID %q: %w", string(headerValue), err) } - commit.Tree = *NewTree(gitRepo, objID) + commit.TreeID = objID case "parent": objID, err := NewIDFromString(string(headerValue)) if err != nil { @@ -48,7 +48,7 @@ func assignCommitFields(gitRepo *Repository, commit *Commit, headerKey string, h // We need this to interpret commits from cat-file or cat-file --batch // // If used as part of a cat-file --batch stream you need to limit the reader to the correct size -func CommitFromReader(gitRepo *Repository, objectID ObjectID, reader io.Reader) (*Commit, error) { +func CommitFromReader(objectID ObjectID, reader io.Reader) (*Commit, error) { commit := &Commit{ ID: objectID, Author: &Signature{}, @@ -74,7 +74,7 @@ func CommitFromReader(gitRepo *Repository, objectID ObjectID, reader io.Reader) k, v, _ := bytes.Cut(line, []byte{' '}) if len(k) != 0 || !inHeader { if headerKey != "" { - if err = assignCommitFields(gitRepo, commit, headerKey, headerValue); err != nil { + if err = assignCommitFields(commit, headerKey, headerValue); err != nil { return nil, fmt.Errorf("unable to parse commit %q: %w", objectID.String(), err) } } diff --git a/modules/git/commit_sha256_test.go b/modules/git/commit_sha256_test.go index 9fb1539dc9..98859fc7b4 100644 --- a/modules/git/commit_sha256_test.go +++ b/modules/git/commit_sha256_test.go @@ -60,12 +60,12 @@ signed commit` 0x94, 0x33, 0xb2, 0xa6, 0x2b, 0x96, 0x4c, 0x17, 0xa4, 0x48, 0x5a, 0xe1, 0x80, 0xf4, 0x5f, 0x59, 0x5d, 0x3e, 0x69, 0xd3, 0x1b, 0x78, 0x60, 0x87, 0x77, 0x5e, 0x28, 0xc6, 0xb6, 0x39, 0x9d, 0xf0, } - gitRepo, err := OpenRepository(t.Context(), filepath.Join(testReposDir, "repo1_bare_sha256")) + gitRepo, err := OpenRepositoryLocal(filepath.Join(testReposDir, "repo1_bare_sha256")) assert.NoError(t, err) assert.NotNil(t, gitRepo) defer gitRepo.Close() - commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString)) + commitFromReader, err := CommitFromReader(sha, strings.NewReader(commitString)) assert.NoError(t, err) require.NotNil(t, commitFromReader) assert.EqualValues(t, sha, commitFromReader.ID) @@ -93,7 +93,7 @@ committer Adam Majer 1698676906 +0100 signed commit`, commitFromReader.Signature.Payload) assert.Equal(t, "Adam Majer ", commitFromReader.Author.String()) - commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n")) + commitFromReader2, err := CommitFromReader(sha, strings.NewReader(commitString+"\n\n")) assert.NoError(t, err) commitFromReader.CommitMessage.MessageRaw += "\n\n" commitFromReader.Signature.Payload += "\n\n" @@ -103,14 +103,14 @@ signed commit`, commitFromReader.Signature.Payload) func TestHasPreviousCommitSha256(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") - repo, err := OpenRepository(t.Context(), bareRepo1Path) + repo, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer repo.Close() - commit, err := repo.GetCommit("f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc") + commit, err := repo.GetCommit(t.Context(), "f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc") assert.NoError(t, err) - objectFormat, err := repo.GetObjectFormat() + objectFormat, err := repo.GetObjectFormat(t.Context()) assert.NoError(t, err) parentSHA := MustIDFromString("b0ec7af4547047f12d5093e37ef8f1b3b5415ed8ee17894d43a34d7d34212e9c") @@ -118,15 +118,15 @@ func TestHasPreviousCommitSha256(t *testing.T) { assert.Equal(t, objectFormat, parentSHA.Type()) assert.Equal(t, "sha256", objectFormat.Name()) - haz, err := commit.HasPreviousCommit(parentSHA) + haz, err := commit.HasPreviousCommit(t.Context(), repo, parentSHA) assert.NoError(t, err) assert.True(t, haz) - hazNot, err := commit.HasPreviousCommit(notParentSHA) + hazNot, err := commit.HasPreviousCommit(t.Context(), repo, notParentSHA) assert.NoError(t, err) assert.False(t, hazNot) - selfNot, err := commit.HasPreviousCommit(commit.ID) + selfNot, err := commit.HasPreviousCommit(t.Context(), repo, commit.ID) assert.NoError(t, err) assert.False(t, selfNot) } diff --git a/modules/git/commit_submodule.go b/modules/git/commit_submodule.go index 5e5f90c20e..4936bf0f71 100644 --- a/modules/git/commit_submodule.go +++ b/modules/git/commit_submodule.go @@ -3,17 +3,19 @@ package git +import "context" + type SubmoduleWebLink struct { RepoWebLink, CommitWebLink string } // GetSubModules get all the submodules of current revision git tree -func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) { +func (c *Commit) GetSubModules(ctx context.Context, gitRepo *Repository) (*ObjectCache[*SubModule], error) { if c.submoduleCache != nil { return c.submoduleCache, nil } - entry, err := c.GetTreeEntryByPath(".gitmodules") + entry, err := c.GetTreeEntryByPath(ctx, gitRepo, ".gitmodules") if err != nil { if _, ok := err.(ErrNotExist); ok { return nil, nil //nolint:nilnil // return nil to indicate that the submodule does not exist @@ -21,7 +23,7 @@ func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) { return nil, err } - rd, err := entry.Blob().DataAsync() + rd, err := entry.Blob(gitRepo).DataAsync(ctx) if err != nil { return nil, err } @@ -37,8 +39,8 @@ func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) { // GetSubModule gets the submodule by the entry name. // It returns "nil, nil" if the submodule does not exist, caller should always remember to check the "nil" -func (c *Commit) GetSubModule(entryName string) (*SubModule, error) { - modules, err := c.GetSubModules() +func (c *Commit) GetSubModule(ctx context.Context, gitRepo *Repository, entryName string) (*SubModule, error) { + modules, err := c.GetSubModules(ctx, gitRepo) if err != nil { return nil, err } diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index 53c4b46e8e..6a148756cc 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -56,12 +56,12 @@ gpgsig -----BEGIN PGP SIGNATURE----- empty commit` sha := &Sha1Hash{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2} - gitRepo, err := OpenRepository(t.Context(), filepath.Join(testReposDir, "repo1_bare")) + gitRepo, err := OpenRepositoryLocal(filepath.Join(testReposDir, "repo1_bare")) assert.NoError(t, err) assert.NotNil(t, gitRepo) defer gitRepo.Close() - commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString)) + commitFromReader, err := CommitFromReader(sha, strings.NewReader(commitString)) assert.NoError(t, err) require.NotNil(t, commitFromReader) assert.EqualValues(t, sha, commitFromReader.ID) @@ -89,7 +89,7 @@ committer silverwind 1563741793 +0200 empty commit`, commitFromReader.Signature.Payload) assert.Equal(t, "silverwind ", commitFromReader.Author.String()) - commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n")) + commitFromReader2, err := CommitFromReader(sha, strings.NewReader(commitString+"\n\n")) assert.NoError(t, err) commitFromReader.CommitMessage.MessageRaw += "\n\n" commitFromReader.Signature.Payload += "\n\n" @@ -120,12 +120,12 @@ gpgsig -----BEGIN PGP SIGNATURE----- ISO-8859-1` commitString = strings.ReplaceAll(commitString, "", " ") sha := &Sha1Hash{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2} - gitRepo, err := OpenRepository(t.Context(), filepath.Join(testReposDir, "repo1_bare")) + gitRepo, err := OpenRepositoryLocal(filepath.Join(testReposDir, "repo1_bare")) assert.NoError(t, err) assert.NotNil(t, gitRepo) defer gitRepo.Close() - commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString)) + commitFromReader, err := CommitFromReader(sha, strings.NewReader(commitString)) assert.NoError(t, err) require.NotNil(t, commitFromReader) assert.EqualValues(t, sha, commitFromReader.ID) @@ -152,7 +152,7 @@ encoding ISO-8859-1 ISO-8859-1`, commitFromReader.Signature.Payload) assert.Equal(t, "KN4CK3R ", commitFromReader.Author.String()) - commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n")) + commitFromReader2, err := CommitFromReader(sha, strings.NewReader(commitString+"\n\n")) assert.NoError(t, err) commitFromReader.CommitMessage.MessageRaw += "\n\n" commitFromReader.Signature.Payload += "\n\n" @@ -162,39 +162,39 @@ ISO-8859-1`, commitFromReader.Signature.Payload) func TestHasPreviousCommit(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - repo, err := OpenRepository(t.Context(), bareRepo1Path) + repo, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer repo.Close() - commit, err := repo.GetCommit("8006ff9adbf0cb94da7dad9e537e53817f9fa5c0") + commit, err := repo.GetCommit(t.Context(), "8006ff9adbf0cb94da7dad9e537e53817f9fa5c0") assert.NoError(t, err) parentSHA := MustIDFromString("8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2") notParentSHA := MustIDFromString("2839944139e0de9737a044f78b0e4b40d989a9e3") - haz, err := commit.HasPreviousCommit(parentSHA) + haz, err := commit.HasPreviousCommit(t.Context(), repo, parentSHA) assert.NoError(t, err) assert.True(t, haz) - hazNot, err := commit.HasPreviousCommit(notParentSHA) + hazNot, err := commit.HasPreviousCommit(t.Context(), repo, notParentSHA) assert.NoError(t, err) assert.False(t, hazNot) - selfNot, err := commit.HasPreviousCommit(commit.ID) + selfNot, err := commit.HasPreviousCommit(t.Context(), repo, commit.ID) assert.NoError(t, err) assert.False(t, selfNot) } func Test_GetCommitBranchStart(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - repo, err := OpenRepository(t.Context(), bareRepo1Path) + repo, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer repo.Close() - commit, err := repo.GetBranchCommit("branch1") + commit, err := repo.GetBranchCommit(t.Context(), "branch1") assert.NoError(t, err) assert.Equal(t, "2839944139e0de9737a044f78b0e4b40d989a9e3", commit.ID.String()) - startCommitID, err := repo.GetCommitBranchStart(os.Environ(), "branch1", commit.ID.String()) + startCommitID, err := repo.GetCommitBranchStart(t.Context(), os.Environ(), "branch1", commit.ID.String()) assert.NoError(t, err) assert.NotEmpty(t, startCommitID) assert.Equal(t, "95bb4d39648ee7e325106df01a621c530863a653", startCommitID) diff --git a/modules/git/config.go b/modules/git/config.go index e6a2ac8817..b7f41251f8 100644 --- a/modules/git/config.go +++ b/modules/git/config.go @@ -21,14 +21,6 @@ func syncGitConfig(ctx context.Context) (err error) { return fmt.Errorf("unable to prepare git home directory %s, err: %w", gitcmd.HomeDir(), err) } - // first, write user's git config options to git config file - // user config options could be overwritten by builtin values later, because if a value is builtin, it must have some special purposes - for k, v := range setting.GitConfig.Options { - if err = configSet(ctx, strings.ToLower(k), v); err != nil { - return err - } - } - // Git requires setting user.name and user.email in order to commit changes - old comment: "if they're not set just add some defaults" // TODO: need to confirm whether users really need to change these values manually. It seems that these values are dummy only and not really used. // If these values are not really used, then they can be set (overwritten) directly without considering about existence. @@ -111,8 +103,18 @@ func syncGitConfig(ctx context.Context) (err error) { } err = configUnsetAll(ctx, "uploadpack.allowAnySHA1InWant", "true") } + if err != nil { + return err + } - return err + // Apply user's git config options last so they take precedence over builtin defaults + for k, v := range setting.GitConfig.Options { + if err = configSet(ctx, strings.ToLower(k), v); err != nil { + return err + } + } + + return nil } func configSet(ctx context.Context, key, value string) error { diff --git a/modules/git/config_test.go b/modules/git/config_test.go index 9e358d4a80..cdfca656e1 100644 --- a/modules/git/config_test.go +++ b/modules/git/config_test.go @@ -10,6 +10,7 @@ import ( "gitea.dev/modules/git/gitcmd" "gitea.dev/modules/setting" + "gitea.dev/modules/test" "github.com/stretchr/testify/assert" ) @@ -55,14 +56,18 @@ func TestGitConfig(t *testing.T) { assert.False(t, gitConfigContains("key-x = *")) } -func TestSyncConfig(t *testing.T) { - oldGitConfig := setting.GitConfig - defer func() { - setting.GitConfig = oldGitConfig - }() +func TestSyncGitConfig(t *testing.T) { + defer test.MockVariableValue(&setting.GitConfig)() + + assert.Empty(t, setting.GitConfig.Options) + assert.NoError(t, syncGitConfig(t.Context())) + assert.True(t, gitConfigContains("commitGraph = true")) // builtin default config setting.GitConfig.Options["sync-test.cfg-key-a"] = "CfgValA" + setting.GitConfig.Options["core.commitgraph"] = "false" assert.NoError(t, syncGitConfig(t.Context())) assert.True(t, gitConfigContains("[sync-test]")) assert.True(t, gitConfigContains("cfg-key-a = CfgValA")) + assert.False(t, gitConfigContains("commitGraph")) // builtin default config can be overridden + assert.True(t, gitConfigContains("commitgraph = false")) // git config key is case-insensitive } diff --git a/modules/git/diff.go b/modules/git/diff.go index eebb3c098f..fbe3988ac6 100644 --- a/modules/git/diff.go +++ b/modules/git/diff.go @@ -27,20 +27,20 @@ const ( ) // GetRawDiff dumps diff results of repository in given commit ID to io.Writer. -func GetRawDiff(repo *Repository, commitID string, diffType RawDiffType, writer io.Writer) (retErr error) { - cmd, err := getRepoRawDiffForFileCmd(repo.Ctx, repo, "", commitID, diffType, "") +func GetRawDiff(ctx context.Context, repo *Repository, commitID string, diffType RawDiffType, writer io.Writer) (retErr error) { + cmd, err := getRepoRawDiffForFileCmd(ctx, repo, "", commitID, diffType, "") if err != nil { return fmt.Errorf("getRepoRawDiffForFileCmd: %w", err) } - return cmd.WithStdoutCopy(writer).RunWithStderr(repo.Ctx) + return cmd.WithStdoutCopy(writer).RunWithStderr(ctx) } // GetFileDiffCutAroundLine cuts the old or new part of the diff of a file around a specific line number func GetFileDiffCutAroundLine( - repo *Repository, startCommit, endCommit, treePath string, + ctx context.Context, repo *Repository, startCommit, endCommit, treePath string, line int64, old bool, numbersOfLine int, ) (ret string, retErr error) { - cmd, err := getRepoRawDiffForFileCmd(repo.Ctx, repo, startCommit, endCommit, RawDiffNormal, treePath) + cmd, err := getRepoRawDiffForFileCmd(ctx, repo, startCommit, endCommit, RawDiffNormal, treePath) if err != nil { return "", fmt.Errorf("getRepoRawDiffForFileCmd: %w", err) } @@ -50,13 +50,13 @@ func GetFileDiffCutAroundLine( ret, err = CutDiffAroundLine(stdoutReader, line, old, numbersOfLine) return err }) - return ret, cmd.RunWithStderr(repo.Ctx) + return ret, cmd.RunWithStderr(ctx) } // getRepoRawDiffForFile returns an io.Reader for the diff results of file in given commit ID // and a "finish" function to wait for the git command and clean up resources after reading is done. -func getRepoRawDiffForFileCmd(_ context.Context, repo *Repository, startCommit, endCommit string, diffType RawDiffType, file string) (*gitcmd.Command, error) { - commit, err := repo.GetCommit(endCommit) +func getRepoRawDiffForFileCmd(ctx context.Context, repo *Repository, startCommit, endCommit string, diffType RawDiffType, file string) (*gitcmd.Command, error) { + commit, err := repo.GetCommit(ctx, endCommit) if err != nil { return nil, err } @@ -75,7 +75,7 @@ func getRepoRawDiffForFileCmd(_ context.Context, repo *Repository, startCommit, } else if commit.ParentCount() == 0 { cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...) } else { - c, err := commit.Parent(0) + c, err := commit.Parent(ctx, repo, 0) if err != nil { return nil, err } @@ -90,7 +90,7 @@ func getRepoRawDiffForFileCmd(_ context.Context, repo *Repository, startCommit, } else if commit.ParentCount() == 0 { cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(endCommit).AddDashesAndList(files...) } else { - c, err := commit.Parent(0) + c, err := commit.Parent(ctx, repo, 0) if err != nil { return nil, err } @@ -292,9 +292,9 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi } // GetAffectedFiles returns the affected files between two commits -func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID string, env []string) ([]string, error) { +func GetAffectedFiles(ctx context.Context, repo *Repository, branchName, oldCommitID, newCommitID string, env []string) ([]string, error) { if oldCommitID == emptySha1ObjectID.String() || oldCommitID == emptySha256ObjectID.String() { - startCommitID, err := repo.GetCommitBranchStart(env, branchName, newCommitID) + startCommitID, err := repo.GetCommitBranchStart(ctx, env, branchName, newCommitID) if err != nil { return nil, err } @@ -323,7 +323,7 @@ func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID str } return scanner.Err() }). - Run(repo.Ctx) + Run(ctx) if err != nil { log.Error("Unable to get affected files for commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err) } diff --git a/modules/git/foreachref/format.go b/modules/git/foreachref/format.go index 87c1c9a4ff..64c9a37a30 100644 --- a/modules/git/foreachref/format.go +++ b/modules/git/foreachref/format.go @@ -16,7 +16,7 @@ var ( ) // Format supports specifying and parsing an output format for 'git -// for-each-ref'. See See git-for-each-ref(1) for available fields. +// for-each-ref'. See git-for-each-ref(1) for available fields. type Format struct { // fieldNames hold %(fieldname)s to be passed to the '--format' flag of // for-each-ref. See git-for-each-ref(1) for available fields. diff --git a/modules/git/git.go b/modules/git/git.go index 0c2deb0281..70164c2699 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -14,6 +14,7 @@ import ( "strings" "gitea.dev/modules/git/gitcmd" + "gitea.dev/modules/globallock" "gitea.dev/modules/log" "gitea.dev/modules/setting" "gitea.dev/modules/tempdir" @@ -195,3 +196,11 @@ func runGitTests(m interface{ Run() int }) int { } return m.Run() } + +func LockConfigAndDo(ctx context.Context, repo RepositoryFacade, fn func(ctx context.Context) error) error { + return globallock.LockAndDo(ctx, "repo-config:"+repo.GitRepoManagedID(), fn) +} + +func LockWriteAndDo(ctx context.Context, repo RepositoryFacade, fn func(ctx context.Context) error) error { + return globallock.LockAndDo(ctx, "repo-write:"+repo.GitRepoManagedID(), fn) +} diff --git a/modules/git/git_test.go b/modules/git/git_test.go index e21cbe449a..af4ee7e3e4 100644 --- a/modules/git/git_test.go +++ b/modules/git/git_test.go @@ -10,6 +10,8 @@ import ( "github.com/stretchr/testify/assert" ) +const testReposDir = "tests/repos/" + func TestMain(m *testing.M) { RunGitTests(m) } diff --git a/modules/git/gitcmd/repo.go b/modules/git/gitcmd/repo.go new file mode 100644 index 0000000000..0a5ab9865b --- /dev/null +++ b/modules/git/gitcmd/repo.go @@ -0,0 +1,66 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitcmd + +import ( + "path/filepath" + + "gitea.dev/modules/setting" +) + +type RepositoryFacade interface { + // GitRepoManagedID returns a "managed id", which should be a cache-key-friendly string. + // e.g.: ID with prefix&suffix or UUID + GitRepoManagedID() string + + // GitRepoLocation returns the location for the git repository. + // * relative path: will be converted to an absolute path by setting.RepoRootPath + // * absolute path: will be used as-is + // * in the future: maybe URI for more flexible definitions + GitRepoLocation() string +} + +func (c *Command) WithRepo(repo RepositoryFacade) *Command { + c.opts.Dir = RepoLocalPath(repo) + return c +} + +func RepoLocalPath(repo RepositoryFacade) string { + repoLoc := repo.GitRepoLocation() + if filepath.IsAbs(repoLoc) { + return repoLoc + } + return filepath.Join(setting.RepoRootPath, filepath.FromSlash(repoLoc)) +} + +type repositoryUnmanaged string + +func (r repositoryUnmanaged) GitRepoManagedID() string { + panic("this repo is not managed by Gitea, can't be used in this managed context") +} + +func (r repositoryUnmanaged) GitRepoLocation() string { + return string(r) +} + +func RepositoryUnmanaged(s string) RepositoryFacade { + return repositoryUnmanaged(s) +} + +type repositoryManaged struct { + id string + loc string +} + +func (r *repositoryManaged) GitRepoManagedID() string { + return r.id +} + +func (r *repositoryManaged) GitRepoLocation() string { + return r.loc +} + +func RepositoryManaged(id, loc string) RepositoryFacade { + return &repositoryManaged{id, loc} +} diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go index b87ac4bea7..8d16e03f19 100644 --- a/modules/git/grep_test.go +++ b/modules/git/grep_test.go @@ -11,7 +11,7 @@ import ( ) func TestGrepSearch(t *testing.T) { - repo, err := OpenRepository(t.Context(), filepath.Join(testReposDir, "language_stats_repo")) + repo, err := OpenRepositoryLocal(filepath.Join(testReposDir, "language_stats_repo")) assert.NoError(t, err) defer repo.Close() @@ -74,7 +74,7 @@ func TestGrepSearch(t *testing.T) { assert.NoError(t, err) assert.Empty(t, res) - res, err = GrepSearch(t.Context(), &Repository{Path: "no-such-git-repo"}, "no-such-content", GrepOptions{}) + res, err = GrepSearch(t.Context(), &Repository{RepositoryBase: RepositoryBase{Path: "no-such-git-repo"}}, "no-such-content", GrepOptions{}) assert.Error(t, err) assert.Empty(t, res) } diff --git a/modules/git/languagestats/language_stats_gogit.go b/modules/git/languagestats/language_stats_gogit.go index 090ac53274..5ddf9096fb 100644 --- a/modules/git/languagestats/language_stats_gogit.go +++ b/modules/git/languagestats/language_stats_gogit.go @@ -7,6 +7,7 @@ package languagestats import ( "bytes" + "context" "io" "gitea.dev/modules/analyze" @@ -21,7 +22,7 @@ import ( ) // GetLanguageStats calculates language stats for git repository at specified commit -func GetLanguageStats(repo *git_module.Repository, commitID string) (map[string]int64, error) { +func GetLanguageStats(ctx context.Context, repo *git_module.Repository, commitID string) (map[string]int64, error) { r, err := git.PlainOpen(repo.Path) if err != nil { return nil, err @@ -42,7 +43,7 @@ func GetLanguageStats(repo *git_module.Repository, commitID string) (map[string] return nil, err } - checker, err := attribute.NewBatchChecker(repo, commitID, attribute.LinguistAttributes) + checker, err := attribute.NewBatchChecker(ctx, repo, commitID, attribute.LinguistAttributes) if err != nil { return nil, err } diff --git a/modules/git/languagestats/language_stats_nogogit.go b/modules/git/languagestats/language_stats_nogogit.go index de431ee33a..4acff61f41 100644 --- a/modules/git/languagestats/language_stats_nogogit.go +++ b/modules/git/languagestats/language_stats_nogogit.go @@ -7,6 +7,7 @@ package languagestats import ( "bytes" + "context" "io" "gitea.dev/modules/analyze" @@ -19,10 +20,10 @@ import ( ) // GetLanguageStats calculates language stats for git repository at specified commit -func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, error) { +func GetLanguageStats(ctx context.Context, repo *git.Repository, commitID string) (map[string]int64, error) { // We will feed the commit IDs in order into cat-file --batch, followed by blobs as necessary. // so let's create a batch stdin and stdout - batch, cancel, err := repo.CatFileBatch(repo.Ctx) + batch, cancel, err := repo.CatFileBatch(ctx) if err != nil { return nil, err } @@ -43,7 +44,7 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, return nil, git.ErrNotExist{ID: commitID} } - commit, err := git.CommitFromReader(repo, sha, io.LimitReader(batchReader, commitInfo.Size)) + commit, err := git.CommitFromReader(sha, io.LimitReader(batchReader, commitInfo.Size)) if err != nil { log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) return nil, err @@ -52,14 +53,12 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, return nil, err } - tree := commit.Tree - - entries, err := tree.ListEntriesRecursiveWithSize() + entries, err := commit.Tree().ListEntriesRecursiveWithSize(ctx, repo) if err != nil { return nil, err } - checker, err := attribute.NewBatchChecker(repo, commitID, attribute.LinguistAttributes) + checker, err := attribute.NewBatchChecker(ctx, repo, commitID, attribute.LinguistAttributes) if err != nil { return nil, err } @@ -79,15 +78,15 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, for _, f := range entries { select { - case <-repo.Ctx.Done(): - return sizes, repo.Ctx.Err() + case <-ctx.Done(): + return sizes, ctx.Err() default: } contentBuf.Reset() content = contentBuf.Bytes() - if f.Size() == 0 { + if f.GetSize(ctx, repo) == 0 { continue } @@ -124,7 +123,7 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, } // this language will always be added to the size - sizes[language] += f.Size() + sizes[language] += f.GetSize(ctx, repo) continue } } @@ -138,7 +137,7 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, // If content can not be read or file is too big just do detection by filename - if f.Size() <= bigFileSize { + if f.GetSize(ctx, repo) <= bigFileSize { info, _, err := batch.QueryContent(f.ID.String()) if err != nil { return nil, err @@ -192,10 +191,10 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, includedLanguage[language] = included } if included || isDetectable.ValueOrDefault(false) { - sizes[language] += f.Size() + sizes[language] += f.GetSize(ctx, repo) } else if len(sizes) == 0 && (firstExcludedLanguage == "" || firstExcludedLanguage == language) { firstExcludedLanguage = language - firstExcludedLanguageSize += f.Size() + firstExcludedLanguageSize += f.GetSize(ctx, repo) } } diff --git a/modules/git/languagestats/language_stats_test.go b/modules/git/languagestats/language_stats_test.go index 1953559f91..979ea17d97 100644 --- a/modules/git/languagestats/language_stats_test.go +++ b/modules/git/languagestats/language_stats_test.go @@ -18,11 +18,11 @@ import ( func TestRepository_GetLanguageStats(t *testing.T) { setting.AppDataPath = t.TempDir() repoPath := "../tests/repos/language_stats_repo" - gitRepo, err := git.OpenRepository(t.Context(), repoPath) + gitRepo, err := git.OpenRepositoryLocal(repoPath) require.NoError(t, err) defer gitRepo.Close() - stats, err := GetLanguageStats(gitRepo, "8fee858da5796dfb37704761701bb8e800ad9ef3") + stats, err := GetLanguageStats(t.Context(), gitRepo, "8fee858da5796dfb37704761701bb8e800ad9ef3") require.NoError(t, err) assert.Equal(t, map[string]int64{ diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go index a7e78ff8b3..2de40cfd0a 100644 --- a/modules/git/last_commit_cache.go +++ b/modules/git/last_commit_cache.go @@ -4,6 +4,7 @@ package git import ( + "context" "crypto/sha256" "fmt" @@ -53,7 +54,7 @@ func (c *LastCommitCache) Put(ref, entryPath, commitID string) error { } // Get gets the last commit information by commit id and entry path -func (c *LastCommitCache) Get(ref, entryPath string) (*Commit, error) { +func (c *LastCommitCache) Get(ctx context.Context, ref, entryPath string) (*Commit, error) { if c == nil || c.cache == nil { return nil, nil //nolint:nilnil // return nil when cache is not available } @@ -71,7 +72,7 @@ func (c *LastCommitCache) Get(ref, entryPath string) (*Commit, error) { } } - commit, err := c.repo.GetCommit(commitID) + commit, err := c.repo.GetCommit(ctx, commitID) if err != nil { return nil, err } @@ -83,18 +84,18 @@ func (c *LastCommitCache) Get(ref, entryPath string) (*Commit, error) { } // GetCommitByPath gets the last commit for the entry in the provided commit -func (c *LastCommitCache) GetCommitByPath(commitID, entryPath string) (*Commit, error) { +func (c *LastCommitCache) GetCommitByPath(ctx context.Context, commitID, entryPath string) (*Commit, error) { sha, err := NewIDFromString(commitID) if err != nil { return nil, err } - lastCommit, err := c.Get(sha.String(), entryPath) + lastCommit, err := c.Get(ctx, sha.String(), entryPath) if err != nil || lastCommit != nil { return lastCommit, err } - lastCommit, err = c.repo.getCommitByPathWithID(sha, entryPath) + lastCommit, err = c.repo.getCommitByPathWithID(ctx, sha, entryPath) if err != nil { return nil, err } diff --git a/modules/git/last_commit_cache_gogit.go b/modules/git/last_commit_cache_gogit.go index 3afc213094..b4b2368d2d 100644 --- a/modules/git/last_commit_cache_gogit.go +++ b/modules/git/last_commit_cache_gogit.go @@ -13,26 +13,26 @@ import ( ) // CacheCommit will cache the commit from the gitRepository -func (c *Commit) CacheCommit(ctx context.Context) error { - if c.repo.LastCommitCache == nil { +func (c *Commit) CacheCommit(ctx context.Context, gitRepo *Repository) error { + if gitRepo.LastCommitCache == nil { return nil } - commitNodeIndex, _ := c.repo.CommitNodeIndex() + commitNodeIndex, _ := gitRepo.CommitNodeIndex() index, err := commitNodeIndex.Get(plumbing.Hash(c.ID.RawValue())) if err != nil { return err } - return c.recursiveCache(ctx, index, &c.Tree, "", 1) + return c.recursiveCache(ctx, gitRepo, index, c.Tree(), "", 1) } -func (c *Commit) recursiveCache(ctx context.Context, index cgobject.CommitNode, tree *Tree, treePath string, level int) error { +func (c *Commit) recursiveCache(ctx context.Context, gitRepo *Repository, index cgobject.CommitNode, tree *Tree, treePath string, level int) error { if level == 0 { return nil } - entries, err := tree.ListEntries() + entries, err := tree.ListEntries(ctx, gitRepo) if err != nil { return err } @@ -44,18 +44,18 @@ func (c *Commit) recursiveCache(ctx context.Context, index cgobject.CommitNode, entryMap[entry.Name()] = entry } - commits, err := GetLastCommitForPaths(ctx, c.repo.LastCommitCache, index, treePath, entryPaths) + commits, err := getLastCommitForPathsByCommitNode(ctx, gitRepo, index, treePath, entryPaths) if err != nil { return err } for entry := range commits { if entryMap[entry].IsDir() { - subTree, err := tree.SubTree(entry) + subTree, err := tree.SubTree(ctx, gitRepo, entry) if err != nil { return err } - if err := c.recursiveCache(ctx, index, subTree, entry, level-1); err != nil { + if err := c.recursiveCache(ctx, gitRepo, index, subTree, entry, level-1); err != nil { return err } } diff --git a/modules/git/last_commit_cache_nogogit.go b/modules/git/last_commit_cache_nogogit.go index 155cb3cb7c..4956cc698e 100644 --- a/modules/git/last_commit_cache_nogogit.go +++ b/modules/git/last_commit_cache_nogogit.go @@ -10,19 +10,18 @@ import ( ) // CacheCommit will cache the commit from the gitRepository -func (c *Commit) CacheCommit(ctx context.Context) error { - if c.repo.LastCommitCache == nil { +func (c *Commit) CacheCommit(ctx context.Context, gitRepo *Repository) error { + if gitRepo.LastCommitCache == nil { return nil } - return c.recursiveCache(ctx, &c.Tree, "", 1) + return c.recursiveCache(ctx, gitRepo, c.Tree(), "", 1) } -func (c *Commit) recursiveCache(ctx context.Context, tree *Tree, treePath string, level int) error { +func (c *Commit) recursiveCache(ctx context.Context, gitRepo *Repository, tree *Tree, treePath string, level int) error { if level == 0 { return nil } - - entries, err := tree.ListEntries() + entries, err := tree.ListEntries(ctx, gitRepo) if err != nil { return err } @@ -32,7 +31,7 @@ func (c *Commit) recursiveCache(ctx context.Context, tree *Tree, treePath string entryPaths[i] = entry.Name() } - _, err = WalkGitLog(ctx, c.repo, c, treePath, entryPaths...) + _, err = walkGitLog(ctx, gitRepo, c, treePath, entryPaths...) if err != nil { return err } @@ -40,11 +39,11 @@ func (c *Commit) recursiveCache(ctx context.Context, tree *Tree, treePath string for _, treeEntry := range entries { // entryMap won't contain "" therefore skip this. if treeEntry.IsDir() { - subTree, err := tree.SubTree(treeEntry.Name()) + subTree, err := tree.SubTree(ctx, gitRepo, treeEntry.Name()) if err != nil { return err } - if err := c.recursiveCache(ctx, subTree, treeEntry.Name(), level-1); err != nil { + if err := c.recursiveCache(ctx, gitRepo, subTree, treeEntry.Name(), level-1); err != nil { return err } } diff --git a/modules/git/log_name_status.go b/modules/git/log_name_status_nogogit.go similarity index 73% rename from modules/git/log_name_status.go rename to modules/git/log_name_status_nogogit.go index a7347a6d8a..05924fca11 100644 --- a/modules/git/log_name_status.go +++ b/modules/git/log_name_status_nogogit.go @@ -1,6 +1,8 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT +//go:build !gogit + package git import ( @@ -18,10 +20,8 @@ import ( "gitea.dev/modules/log" ) -// LogNameStatusRepo opens git log --raw in the provided repo and returns a stdin pipe, a stdout reader and cancel function -func LogNameStatusRepo(ctx context.Context, repository, head, treepath string, paths ...string) (*bufio.Reader, func()) { - // Lets also create a context so that we can absolutely ensure that the command should die when we're done - +// logNameStatusRepo opens git log --raw in the provided repo and returns a parser +func logNameStatusRepo(ctx context.Context, repository, head, treepath string, paths ...string) *logNameStatusRepoParser { cmd := gitcmd.NewCommand() cmd.AddArguments("log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "-t", "-z").AddDynamicArguments(head) @@ -54,77 +54,62 @@ func LogNameStatusRepo(ctx context.Context, repository, head, treepath string, p ctx, ctxCancel := context.WithCancel(ctx) go func() { err := cmd.WithDir(repository).RunWithStderr(ctx) - if err != nil && !errors.Is(err, context.Canceled) { + if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { log.Error("Unable to run git command %v: %v", cmd.LogString(), err) } }() bufReader := bufio.NewReaderSize(stdoutReader, 32*1024) - - return bufReader, func() { - ctxCancel() - stdoutReaderClose() + return &logNameStatusRepoParser{ + treepath: treepath, + paths: paths, + rd: bufReader, + close: func() { + ctxCancel() + stdoutReaderClose() + }, } } -// LogNameStatusRepoParser parses a git log raw output from LogRawRepo -type LogNameStatusRepoParser struct { +// logNameStatusRepoParser parses a git log raw output from LogRawRepo +type logNameStatusRepoParser struct { treepath string paths []string next []byte buffull bool rd *bufio.Reader - cancel func() + close func() } -// NewLogNameStatusRepoParser returns a new parser for a git log raw output -func NewLogNameStatusRepoParser(ctx context.Context, repository, head, treepath string, paths ...string) *LogNameStatusRepoParser { - rd, cancel := LogNameStatusRepo(ctx, repository, head, treepath, paths...) - return &LogNameStatusRepoParser{ - treepath: treepath, - paths: paths, - rd: rd, - cancel: cancel, - } -} - -// LogNameStatusCommitData represents a commit artefact from git log raw -type LogNameStatusCommitData struct { +// logNameStatusCommitData represents a commit artifact from git log raw +type logNameStatusCommitData struct { CommitID string ParentIDs []string Paths []bool } -// Next returns the next LogStatusCommitData -func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int, changed []bool, maxpathlen int) (*LogNameStatusCommitData, error) { +// walkNext returns the next LogStatusCommitData +func (g *logNameStatusRepoParser) walkNext(treepath string, paths2ids map[string]int, changed []bool, maxpathlen int) (*logNameStatusCommitData, error) { var err error if len(g.next) == 0 { g.buffull = false g.next, err = g.rd.ReadSlice('\x00') - if err != nil { - switch err { - case bufio.ErrBufferFull: - g.buffull = true - case io.EOF: - return nil, nil //nolint:nilnil // return nil to signal EOF - default: - return nil, err - } + switch { + case errors.Is(err, bufio.ErrBufferFull): + g.buffull = true + case err != nil: + return nil, err } } - ret := LogNameStatusCommitData{} + ret := logNameStatusCommitData{} if bytes.Equal(g.next, []byte("commit\000")) { g.next, err = g.rd.ReadSlice('\x00') - if err != nil { - switch err { - case bufio.ErrBufferFull: - g.buffull = true - case io.EOF: - return nil, nil //nolint:nilnil // return nil to signal EOF - default: - return nil, err - } + switch { + case errors.Is(err, bufio.ErrBufferFull): + g.buffull = true + case err != nil: + return nil, err } } @@ -273,21 +258,17 @@ diffloop: } } -// Close closes the parser -func (g *LogNameStatusRepoParser) Close() { - g.cancel() -} +var walkGitLogDebugBeforeNext func() // is used to simulate various edge git process cases -// WalkGitLog walks the git log --name-status for the head commit in the provided treepath and files -func WalkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath string, paths ...string) (map[string]string, error) { +// walkGitLog walks the git log --name-status for the head commit in the provided treepath and files +func walkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath string, paths ...string) (map[string]string, error) { headRef := head.ID.String() - - tree, err := head.SubTree(treepath) + tree, err := head.SubTree(ctx, repo, treepath) if err != nil { return nil, err } - entries, err := tree.ListEntries() + entries, err := tree.ListEntries(ctx, repo) if err != nil { return nil, err } @@ -322,11 +303,9 @@ func WalkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath st } } - g := NewLogNameStatusRepoParser(ctx, repo.Path, head.ID.String(), treepath, paths...) - // don't use defer g.Close() here as g may change its value - instead wrap in a func - defer func() { - g.Close() - }() + g := logNameStatusRepo(ctx, repo.Path, head.ID.String(), treepath, paths...) + // don't use defer g.cancel() here as g may change its value - instead wrap in a func + defer func() { g.close() }() results := make([]string, len(paths)) remaining := len(paths) @@ -340,25 +319,16 @@ func WalkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath st heaploop: for { - select { - case <-ctx.Done(): - if ctx.Err() == context.DeadlineExceeded { - break heaploop - } - g.Close() - return nil, ctx.Err() - default: + if walkGitLogDebugBeforeNext != nil { + walkGitLogDebugBeforeNext() } - current, err := g.Next(treepath, path2idx, changed, maxpathlen) - if err != nil { - if errors.Is(err, context.DeadlineExceeded) { - break heaploop - } - g.Close() - return nil, err - } - if current == nil { - break heaploop + current, err := g.walkNext(treepath, path2idx, changed, maxpathlen) + if ctx.Err() != nil { + break heaploop // context is either canceled or deadline exceeded - break the loop and return what we have so far + } else if errors.Is(err, io.EOF) { + break heaploop // reached to the end of log output + } else if err != nil { + return nil, err // other unknown errors } parentRemaining.Remove(current.CommitID) for i, found := range current.Paths { @@ -395,14 +365,14 @@ heaploop: if remaining <= nextRestart { commitSinceNextRestart++ if 4*commitSinceNextRestart > 3*commitSinceLastEmptyParent { - g.Close() remainingPaths := make([]string, 0, len(paths)) for i, pth := range paths { if results[i] == "" { remainingPaths = append(remainingPaths, pth) } } - g = NewLogNameStatusRepoParser(ctx, repo.Path, lastEmptyParent, treepath, remainingPaths...) + g.close() + g = logNameStatusRepo(ctx, repo.Path, lastEmptyParent, treepath, remainingPaths...) parentRemaining = make(container.Set[string]) nextRestart = (remaining * 3) / 4 continue heaploop @@ -410,7 +380,6 @@ heaploop: } parentRemaining.AddMultiple(current.ParentIDs...) } - g.Close() resultsMap := map[string]string{} for i, pth := range paths { diff --git a/modules/git/notes.go b/modules/git/notes.go index 63539cb3a2..95bdeac8aa 100644 --- a/modules/git/notes.go +++ b/modules/git/notes.go @@ -3,6 +3,14 @@ package git +import ( + "context" + "io" + "strings" + + "gitea.dev/modules/log" +) + // NotesRef is the git ref where Gitea will look for git-notes data. // The value ("refs/notes/commits") is the default ref used by git-notes. const NotesRef = "refs/notes/commits" @@ -12,3 +20,80 @@ type Note struct { Message []byte Commit *Commit } + +// GetNote retrieves the git-notes data for a given commit. +// FIXME: Add LastCommitCache support +func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error { + log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path) + notes, err := repo.GetCommit(ctx, NotesRef) + if err != nil { + if IsErrNotExist(err) { + return err + } + log.Error("Unable to get commit from ref %q. Error: %v", NotesRef, err) + return err + } + + path := "" + + tree := notes.Tree() + log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", tree.ID, commitID) + + var entry *TreeEntry + originalCommitID := commitID + for len(commitID) > 2 { + entry, err = tree.GetTreeEntryByPath(ctx, repo, commitID) + if err == nil { + path += commitID + break + } + if IsErrNotExist(err) { + tree, err = tree.SubTree(ctx, repo, commitID[0:2]) + path += commitID[0:2] + "/" + commitID = commitID[2:] + } + if err != nil { + // Err may have been updated by the SubTree we need to recheck if it's again an ErrNotExist + if !IsErrNotExist(err) { + log.Error("Unable to find git note corresponding to the commit %q. Error: %v", originalCommitID, err) + } + return err + } + } + + blob := entry.Blob(repo) + dataRc, err := blob.DataAsync(ctx) + if err != nil { + log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err) + return err + } + closed := false + defer func() { + if !closed { + _ = dataRc.Close() + } + }() + d, err := io.ReadAll(dataRc) + if err != nil { + log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err) + return err + } + _ = dataRc.Close() + closed = true + note.Message = d + + treePath := "" + if idx := strings.LastIndex(path, "/"); idx > -1 { + treePath = path[:idx] + path = path[idx+1:] + } + + lastCommits, err := GetLastCommitForPaths(ctx, repo, notes, treePath, []string{path}) + if err != nil { + log.Error("Unable to get the commit for the path %q. Error: %v", treePath, err) + return err + } + note.Commit = lastCommits[path] + + return nil +} diff --git a/modules/git/notes_gogit.go b/modules/git/notes_gogit.go deleted file mode 100644 index a4cf575ad4..0000000000 --- a/modules/git/notes_gogit.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build gogit - -package git - -import ( - "context" - "fmt" - "io" - "strings" - - "gitea.dev/modules/log" - - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" -) - -// GetNote retrieves the git-notes data for a given commit. -// FIXME: Add LastCommitCache support -func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error { - log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path) - notes, err := repo.GetCommit(NotesRef) - if err != nil { - if IsErrNotExist(err) { - return err - } - log.Error("Unable to get commit from ref %q. Error: %v", NotesRef, err) - return err - } - - remainingCommitID := commitID - var path strings.Builder - currentTree, err := notes.Tree.gogitTreeObject() - if err != nil { - return fmt.Errorf("unable to get tree object for notes commit %q: %w", notes.ID.String(), err) - } - - log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", currentTree.Entries[0].Name, commitID) - var file *object.File - for len(remainingCommitID) > 2 { - file, err = currentTree.File(remainingCommitID) - if err == nil { - path.WriteString(remainingCommitID) - break - } - if err == object.ErrFileNotFound { - currentTree, err = currentTree.Tree(remainingCommitID[0:2]) - path.WriteString(remainingCommitID[0:2] + "/") - remainingCommitID = remainingCommitID[2:] - } - if err != nil { - if err == object.ErrDirectoryNotFound { - return ErrNotExist{ID: remainingCommitID, RelPath: path.String()} - } - log.Error("Unable to find git note corresponding to the commit %q. Error: %v", commitID, err) - return err - } - } - - blob := file.Blob - dataRc, err := blob.Reader() - if err != nil { - log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err) - return err - } - - defer dataRc.Close() - d, err := io.ReadAll(dataRc) - if err != nil { - log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err) - return err - } - note.Message = d - - commitNodeIndex, commitGraphFile := repo.CommitNodeIndex() - if commitGraphFile != nil { - defer commitGraphFile.Close() - } - - commitNode, err := commitNodeIndex.Get(plumbing.Hash(notes.ID.RawValue())) - if err != nil { - return err - } - - lastCommits, err := GetLastCommitForPaths(ctx, nil, commitNode, "", []string{path.String()}) - if err != nil { - log.Error("Unable to get the commit for the path %q. Error: %v", path.String(), err) - return err - } - note.Commit = lastCommits[path.String()] - - return nil -} diff --git a/modules/git/notes_nogogit.go b/modules/git/notes_nogogit.go deleted file mode 100644 index 09ee53f693..0000000000 --- a/modules/git/notes_nogogit.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build !gogit - -package git - -import ( - "context" - "io" - "strings" - - "gitea.dev/modules/log" -) - -// GetNote retrieves the git-notes data for a given commit. -// FIXME: Add LastCommitCache support -func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) error { - log.Trace("Searching for git note corresponding to the commit %q in the repository %q", commitID, repo.Path) - notes, err := repo.GetCommit(NotesRef) - if err != nil { - if IsErrNotExist(err) { - return err - } - log.Error("Unable to get commit from ref %q. Error: %v", NotesRef, err) - return err - } - - path := "" - - tree := ¬es.Tree - log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", tree.ID, commitID) - - var entry *TreeEntry - originalCommitID := commitID - for len(commitID) > 2 { - entry, err = tree.GetTreeEntryByPath(commitID) - if err == nil { - path += commitID - break - } - if IsErrNotExist(err) { - tree, err = tree.SubTree(commitID[0:2]) - path += commitID[0:2] + "/" - commitID = commitID[2:] - } - if err != nil { - // Err may have been updated by the SubTree we need to recheck if it's again an ErrNotExist - if !IsErrNotExist(err) { - log.Error("Unable to find git note corresponding to the commit %q. Error: %v", originalCommitID, err) - } - return err - } - } - - blob := entry.Blob() - dataRc, err := blob.DataAsync() - if err != nil { - log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err) - return err - } - closed := false - defer func() { - if !closed { - _ = dataRc.Close() - } - }() - d, err := io.ReadAll(dataRc) - if err != nil { - log.Error("Unable to read blob with ID %q. Error: %v", blob.ID, err) - return err - } - _ = dataRc.Close() - closed = true - note.Message = d - - treePath := "" - if idx := strings.LastIndex(path, "/"); idx > -1 { - treePath = path[:idx] - path = path[idx+1:] - } - - lastCommits, err := GetLastCommitForPaths(ctx, notes, treePath, []string{path}) - if err != nil { - log.Error("Unable to get the commit for the path %q. Error: %v", treePath, err) - return err - } - note.Commit = lastCommits[path] - - return nil -} diff --git a/modules/git/notes_test.go b/modules/git/notes_test.go index 5abb68b102..1750ea4ae1 100644 --- a/modules/git/notes_test.go +++ b/modules/git/notes_test.go @@ -12,7 +12,7 @@ import ( func TestGetNotes(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) + bareRepo1, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer bareRepo1.Close() @@ -25,7 +25,7 @@ func TestGetNotes(t *testing.T) { func TestGetNestedNotes(t *testing.T) { repoPath := filepath.Join(testReposDir, "repo3_notes") - repo, err := OpenRepository(t.Context(), repoPath) + repo, err := OpenRepositoryLocal(repoPath) assert.NoError(t, err) defer repo.Close() @@ -40,7 +40,7 @@ func TestGetNestedNotes(t *testing.T) { func TestGetNonExistentNotes(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) + bareRepo1, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer bareRepo1.Close() diff --git a/modules/git/object_format.go b/modules/git/object_format.go index 242d782e17..7c340d1473 100644 --- a/modules/git/object_format.go +++ b/modules/git/object_format.go @@ -10,10 +10,10 @@ import ( "strconv" ) -// sha1Pattern can be used to determine if a string is an valid sha +// sha1Pattern can be used to determine if a string is a valid sha var sha1Pattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`) -// sha256Pattern can be used to determine if a string is an valid sha +// sha256Pattern can be used to determine if a string is a valid sha var sha256Pattern = regexp.MustCompile(`^[0-9a-f]{4,64}$`) type ObjectFormat interface { diff --git a/modules/git/pipeline/lfs_gogit.go b/modules/git/pipeline/lfs_gogit.go index c9685c053d..add632f694 100644 --- a/modules/git/pipeline/lfs_gogit.go +++ b/modules/git/pipeline/lfs_gogit.go @@ -6,6 +6,7 @@ package pipeline import ( + "context" "fmt" "io" "sort" @@ -19,7 +20,7 @@ import ( ) // FindLFSFile finds commits that contain a provided pointer file hash -func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) { +func FindLFSFile(ctx context.Context, repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) { resultsMap := map[string]*LFSResult{} results := make([]*LFSResult, 0) @@ -80,6 +81,6 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err } sort.Sort(lfsResultSlice(results)) - err = fillResultNameRev(repo.Ctx, repo.Path, results) + err = fillResultNameRev(ctx, repo.Path, results) return results, err } diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go index 8b596e9b05..656c38e5a6 100644 --- a/modules/git/pipeline/lfs_nogogit.go +++ b/modules/git/pipeline/lfs_nogogit.go @@ -8,6 +8,7 @@ package pipeline import ( "bufio" "bytes" + "context" "io" "sort" @@ -16,24 +17,24 @@ import ( ) // FindLFSFile finds commits that contain a provided pointer file hash -func FindLFSFile(repo *git.Repository, objectID git.ObjectID) (results []*LFSResult, _ error) { +func FindLFSFile(ctx context.Context, repo *git.Repository, objectID git.ObjectID) (results []*LFSResult, _ error) { cmd := gitcmd.NewCommand("rev-list", "--all") revListReader, revListReaderClose := cmd.MakeStdoutPipe() defer revListReaderClose() err := cmd.WithDir(repo.Path). WithPipelineFunc(func(context gitcmd.Context) (err error) { - results, err = findLFSFileFunc(repo, objectID, revListReader) + results, err = findLFSFileFunc(ctx, repo, objectID, revListReader) return err - }).RunWithStderr(repo.Ctx) + }).RunWithStderr(ctx) return results, err } -func findLFSFileFunc(repo *git.Repository, objectID git.ObjectID, revListReader io.Reader) ([]*LFSResult, error) { +func findLFSFileFunc(ctx context.Context, repo *git.Repository, objectID git.ObjectID, revListReader io.Reader) ([]*LFSResult, error) { resultsMap := map[string]*LFSResult{} results := make([]*LFSResult, 0) // Next feed the commits in order into cat-file --batch, followed by their trees and sub trees as necessary. // so let's create a batch stdin and stdout - batch, cancel, err := repo.CatFileBatch(repo.Ctx) + batch, cancel, err := repo.CatFileBatch(ctx) if err != nil { return nil, err } @@ -72,7 +73,7 @@ func findLFSFileFunc(repo *git.Repository, objectID git.ObjectID, revListReader continue case "commit": // Read in the commit to get its tree and in case this is one of the last used commits - curCommit, err = git.CommitFromReader(repo, git.MustIDFromString(commitID), io.LimitReader(batchReader, info.Size)) + curCommit, err = git.CommitFromReader(git.MustIDFromString(commitID), io.LimitReader(batchReader, info.Size)) if err != nil { return nil, err } @@ -80,7 +81,7 @@ func findLFSFileFunc(repo *git.Repository, objectID git.ObjectID, revListReader return nil, err } - if info, _, err = batch.QueryContent(curCommit.Tree.ID.String()); err != nil { + if info, _, err = batch.QueryContent(curCommit.TreeID.String()); err != nil { return nil, err } curPath = "" @@ -145,6 +146,6 @@ func findLFSFileFunc(repo *git.Repository, objectID git.ObjectID, revListReader } sort.Sort(lfsResultSlice(results)) - err = fillResultNameRev(repo.Ctx, repo.Path, results) + err = fillResultNameRev(ctx, repo.Path, results) return results, err } diff --git a/modules/git/pipeline/lfs_test.go b/modules/git/pipeline/lfs_test.go index 97cdbea5ca..0dd8d11471 100644 --- a/modules/git/pipeline/lfs_test.go +++ b/modules/git/pipeline/lfs_test.go @@ -15,13 +15,13 @@ import ( func TestFindLFSFile(t *testing.T) { repoPath := "../../../tests/gitea-repositories-meta/user2/lfs.git" - gitRepo, err := git.OpenRepository(t.Context(), repoPath) + gitRepo, err := git.OpenRepositoryLocal(repoPath) require.NoError(t, err) defer gitRepo.Close() objectID := git.MustIDFromString("2b6c6c4eaefa24b22f2092c3d54b263ff26feb58") - stats, err := FindLFSFile(gitRepo, objectID) + stats, err := FindLFSFile(t.Context(), gitRepo, objectID) require.NoError(t, err) tm, err := time.Parse(time.RFC3339, "2022-12-21T17:56:42-05:00") diff --git a/modules/git/ref.go b/modules/git/ref.go index 7d0bbcbae9..ec6e237eb2 100644 --- a/modules/git/ref.go +++ b/modules/git/ref.go @@ -4,6 +4,7 @@ package git import ( + "context" "regexp" "strings" @@ -50,8 +51,8 @@ type Reference struct { } // Commit return the commit of the reference -func (ref *Reference) Commit() (*Commit, error) { - return ref.repo.getCommit(ref.Object) +func (ref *Reference) Commit(ctx context.Context) (*Commit, error) { + return ref.repo.getCommit(ctx, ref.Object) } // ShortName returns the short name of the reference @@ -228,13 +229,16 @@ func (ref RefName) RefWebLinkPath() string { return string(refType) + "/" + util.PathEscapeSegments(ref.ShortName()) } -func ParseRefSuffix(ref string) (string, string) { +func ParseRefSuffix(ref string) (refName, refSuffix string) { // Partially support https://git-scm.com/docs/gitrevisions - if idx := strings.Index(ref, "@{"); idx != -1 { - return ref[:idx], ref[idx:] + suffixIdx := -1 // earliest suffix mark, so a combined suffix like "main~2^" stays intact + for _, mark := range []string{"@{", "^", "~"} { + if idx := strings.Index(ref, mark); idx != -1 && (suffixIdx == -1 || idx < suffixIdx) { + suffixIdx = idx + } } - if idx := strings.Index(ref, "^"); idx != -1 { - return ref[:idx], ref[idx:] + if suffixIdx == -1 { + return ref, "" } - return ref, "" + return ref[:suffixIdx], ref[suffixIdx:] } diff --git a/modules/git/ref_test.go b/modules/git/ref_test.go index 5397191561..5016eea9d5 100644 --- a/modules/git/ref_test.go +++ b/modules/git/ref_test.go @@ -37,3 +37,22 @@ func TestRefWebLinkPath(t *testing.T) { assert.Equal(t, "tag/foo", RefName("refs/tags/foo").RefWebLinkPath()) assert.Equal(t, "commit/c0ffee", RefName("c0ffee").RefWebLinkPath()) } + +func TestParseRefSuffix(t *testing.T) { + cases := []struct { + ref, name, suffix string + }{ + {"main", "main", ""}, + {"main^", "main", "^"}, + {"main^2", "main", "^2"}, + {"main~3", "main", "~3"}, + {"main@{yesterday}", "main", "@{yesterday}"}, + {"main~2^", "main", "~2^"}, + {"main^~2", "main", "^~2"}, + } + for _, c := range cases { + name, suffix := ParseRefSuffix(c.ref) + assert.Equal(t, c.name, name, "ref: %s", c.ref) + assert.Equal(t, c.suffix, suffix, "ref: %s", c.ref) + } +} diff --git a/modules/git/repo.go b/modules/git/repo.go index 289033332b..6c763c0c80 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -5,50 +5,80 @@ package git import ( - "bytes" "context" "fmt" "net/url" "os" "path" + "path/filepath" "strconv" "strings" "time" "gitea.dev/modules/git/gitcmd" "gitea.dev/modules/proxy" + "gitea.dev/modules/setting" + "gitea.dev/modules/util" ) -const prettyLogFormat = `--pretty=format:%H` +type RepositoryFacade = gitcmd.RepositoryFacade -func (repo *Repository) ShowPrettyFormatLogToList(ctx context.Context, revisionRange string) ([]*Commit, error) { - // avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git [...] -- [...]' - logs, _, err := gitcmd.NewCommand("log").AddArguments(prettyLogFormat). - AddDynamicArguments(revisionRange).AddArguments("--").WithDir(repo.Path). - RunStdBytes(ctx) +type RepositoryBase struct { + Path string // absolute path + + LastCommitCache *LastCommitCache + + repoFacade RepositoryFacade + tagCache *ObjectCache[*Tag] + objectFormatCache ObjectFormat +} + +var _ gitcmd.RepositoryFacade = (*Repository)(nil) + +func (repo *Repository) GitRepoManagedID() string { + return repo.repoFacade.GitRepoManagedID() +} + +func (repo *Repository) GitRepoLocation() string { + return repo.repoFacade.GitRepoLocation() +} + +func OpenRepository(repo RepositoryFacade) (*Repository, error) { + repoPath := gitcmd.RepoLocalPath(repo) + exist, err := util.IsDir(repoPath) if err != nil { return nil, err } - return repo.parsePrettyFormatLogToList(logs) + if !exist { + return nil, util.NewNotExistErrorf("no such file or directory") + } + gitRepo := &Repository{ + RepositoryBase: RepositoryBase{Path: repoPath, tagCache: newObjectCache[*Tag](), repoFacade: repo}, + } + if err = openRepositoryInternal(gitRepo); err != nil { + return nil, err + } + return gitRepo, nil } -func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, error) { - var commits []*Commit - if len(logs) == 0 { - return commits, nil - } - - parts := bytes.SplitSeq(logs, []byte{'\n'}) - - for commitID := range parts { - commit, err := repo.GetCommit(string(commitID)) +func OpenRepositoryLocal(localPath string) (_ *Repository, err error) { + if !filepath.IsAbs(localPath) { + localPath, err = filepath.Abs(localPath) if err != nil { return nil, err } - commits = append(commits, commit) } + return OpenRepository(gitcmd.RepositoryUnmanaged(localPath)) +} - return commits, nil +func (repo *Repository) Close() error { + if repo == nil { + setting.PanicInDevOrTesting("don't close a nil repository") + return nil + } + repo.LastCommitCache = nil + repo.tagCache = nil + return repo.closeInternal() } // IsRepoURLAccessible checks if given repository URL is accessible. @@ -81,12 +111,12 @@ func InitRepository(ctx context.Context, repoPath string, bare bool, objectForma } // IsEmpty Check if repository is empty. -func (repo *Repository) IsEmpty() (bool, error) { +func (repo *Repository) IsEmpty(ctx context.Context) (bool, error) { stdout, _, err := gitcmd.NewCommand(). AddOptionFormat("--git-dir=%s", repo.Path). AddArguments("rev-list", "-n", "1", "--all"). WithDir(repo.Path). - RunStdString(repo.Ctx) + RunStdString(ctx) if err != nil { if (gitcmd.IsErrorExitCode(err, 1) && err.Stderr() == "") || gitcmd.IsErrorExitCode(err, 129) { // git 2.11 exits with 129 if the repo is empty diff --git a/modules/git/repo_base_gogit.go b/modules/git/repo_base_gogit.go index 83f12f3f9e..b7d6c69d0f 100644 --- a/modules/git/repo_base_gogit.go +++ b/modules/git/repo_base_gogit.go @@ -7,12 +7,9 @@ package git import ( - "context" "path/filepath" - gitealog "gitea.dev/modules/log" "gitea.dev/modules/setting" - "gitea.dev/modules/util" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/osfs" @@ -24,40 +21,20 @@ import ( const isGogit = true -// Repository represents a Git repository. type Repository struct { - Path string - - tagCache *ObjectCache[*Tag] + RepositoryBase gogitRepo *gogit.Repository gogitStorage *filesystem.Storage - - Ctx context.Context - LastCommitCache *LastCommitCache - objectFormat ObjectFormat } -// OpenRepository opens the repository at the given path within the context.Context -func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { - repoPath, err := filepath.Abs(repoPath) - if err != nil { - return nil, err - } - exist, err := util.IsDir(repoPath) - if err != nil { - return nil, err - } - if !exist { - return nil, util.NewNotExistErrorf("no such file or directory") - } - - fs := osfs.New(repoPath) - _, err = fs.Stat(".git") +func openRepositoryInternal(gitRepo *Repository) error { + fs := osfs.New(gitRepo.Path) + _, err := fs.Stat(".git") if err == nil { fs, err = fs.Chroot(".git") if err != nil { - return nil, err + return err } } // the "clone --shared" repo doesn't work well with go-git AlternativeFS, https://github.com/go-git/go-git/issues/1006 @@ -68,34 +45,23 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { } else { altFs = osfs.New("/") } - storage := filesystem.NewStorageWithOptions(fs, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true, LargeObjectThreshold: setting.Git.LargeObjectThreshold, AlternatesFS: altFs}) - gogitRepo, err := gogit.Open(storage, fs) + gitRepo.objectFormatCache = ParseGogitHash(plumbing.ZeroHash).Type() + gitRepo.gogitStorage = filesystem.NewStorageWithOptions(fs, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true, LargeObjectThreshold: setting.Git.LargeObjectThreshold, AlternatesFS: altFs}) + gitRepo.gogitRepo, err = gogit.Open(gitRepo.gogitStorage, fs) if err != nil { - return nil, err + _ = gitRepo.gogitStorage.Close() + return err } - - return &Repository{ - Path: repoPath, - gogitRepo: gogitRepo, - gogitStorage: storage, - tagCache: newObjectCache[*Tag](), - Ctx: ctx, - objectFormat: ParseGogitHash(plumbing.ZeroHash).Type(), - }, nil + return nil } -// Close this repository, in particular close the underlying gogitStorage if this is not nil -func (repo *Repository) Close() error { - if repo == nil || repo.gogitStorage == nil { +func (repo *Repository) closeInternal() error { + if repo.gogitStorage == nil { return nil } - if err := repo.gogitStorage.Close(); err != nil { - gitealog.Error("Error closing storage: %v", err) - } + err := repo.gogitStorage.Close() repo.gogitStorage = nil - repo.LastCommitCache = nil - repo.tagCache = nil - return nil + return err } // GoGitRepo gets the go-git repo representation diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go index 2206849544..b6b08f76b3 100644 --- a/modules/git/repo_base_nogogit.go +++ b/modules/git/repo_base_nogogit.go @@ -8,50 +8,23 @@ package git import ( "context" - "path/filepath" "sync" "gitea.dev/modules/log" - "gitea.dev/modules/util" ) const isGogit = false -// Repository represents a Git repository. type Repository struct { - Path string - - tagCache *ObjectCache[*Tag] + RepositoryBase mu sync.Mutex catFileBatchCloser CatFileBatchCloser catFileBatchInUse bool - - Ctx context.Context - LastCommitCache *LastCommitCache - - objectFormat ObjectFormat } -// OpenRepository opens the repository at the given path with the provided context. -func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { - repoPath, err := filepath.Abs(repoPath) - if err != nil { - return nil, err - } - exist, err := util.IsDir(repoPath) - if err != nil { - return nil, err - } - if !exist { - return nil, util.NewNotExistErrorf("no such file or directory") - } - - return &Repository{ - Path: repoPath, - tagCache: newObjectCache[*Tag](), - Ctx: ctx, - }, nil +func openRepositoryInternal(_ *Repository) error { + return nil } // CatFileBatch obtains a "batch object provider" for this repository. @@ -60,8 +33,16 @@ func (repo *Repository) CatFileBatch(ctx context.Context) (_ CatFileBatch, close repo.mu.Lock() defer repo.mu.Unlock() + if repo.catFileBatchCloser != nil && !repo.catFileBatchInUse { + if ctx != repo.catFileBatchCloser.Context() { + repo.catFileBatchCloser.Close() + repo.catFileBatchCloser = nil + repo.catFileBatchInUse = false + } + } + if repo.catFileBatchCloser == nil { - repo.catFileBatchCloser, err = NewBatch(ctx, repo.Path) + repo.catFileBatchCloser, err = NewBatch(ctx, repo) if err != nil { repo.catFileBatchCloser = nil // otherwise it is "interface(nil)" and will cause wrong logic return nil, nil, err @@ -78,17 +59,14 @@ func (repo *Repository) CatFileBatch(ctx context.Context) (_ CatFileBatch, close } log.Debug("Opening temporary cat file batch for: %s", repo.Path) - tempBatch, err := NewBatch(ctx, repo.Path) + tempBatch, err := NewBatch(ctx, repo) if err != nil { return nil, nil, err } return tempBatch, tempBatch.Close, nil } -func (repo *Repository) Close() error { - if repo == nil { - return nil - } +func (repo *Repository) closeInternal() error { repo.mu.Lock() defer repo.mu.Unlock() if repo.catFileBatchCloser != nil { @@ -96,7 +74,5 @@ func (repo *Repository) Close() error { repo.catFileBatchCloser = nil repo.catFileBatchInUse = false } - repo.LastCommitCache = nil - repo.tagCache = nil return nil } diff --git a/modules/git/repo_base_nogogit_test.go b/modules/git/repo_base_nogogit_test.go index a12bbb73c2..776f25b5e6 100644 --- a/modules/git/repo_base_nogogit_test.go +++ b/modules/git/repo_base_nogogit_test.go @@ -6,6 +6,7 @@ package git import ( + "os" "path/filepath" "testing" @@ -14,9 +15,13 @@ import ( func TestRepoCatFileBatch(t *testing.T) { t.Run("MissingRepoAndClose", func(t *testing.T) { - repo, err := OpenRepository(t.Context(), filepath.Join(testReposDir, "repo1_bare")) + testDir := filepath.Join(t.TempDir(), "testdir") + _ = os.Mkdir(testDir, 0o755) + repo, err := OpenRepositoryLocal(testDir) + require.NoError(t, err) + // when the repo is missing (it usually occurs during testing because the fixtures are synced frequently) + err = os.Remove(testDir) require.NoError(t, err) - repo.Path = "/no-such" // when the repo is missing (it usually occurs during testing because the fixtures are synced frequently) _, _, err = repo.CatFileBatch(t.Context()) require.Error(t, err) require.NoError(t, repo.Close()) // shouldn't panic diff --git a/modules/git/repo_blob_test.go b/modules/git/repo_blob_test.go index f07b31d236..3d333d6883 100644 --- a/modules/git/repo_blob_test.go +++ b/modules/git/repo_blob_test.go @@ -14,7 +14,7 @@ import ( func TestRepository_GetBlob_Found(t *testing.T) { repoPath := filepath.Join(testReposDir, "repo1_bare") - r, err := OpenRepository(t.Context(), repoPath) + r, err := OpenRepositoryLocal(repoPath) assert.NoError(t, err) defer r.Close() @@ -30,7 +30,7 @@ func TestRepository_GetBlob_Found(t *testing.T) { blob, err := r.GetBlob(testCase.OID) assert.NoError(t, err) - dataReader, err := blob.DataAsync() + dataReader, err := blob.DataAsync(t.Context()) assert.NoError(t, err) data, err := io.ReadAll(dataReader) @@ -42,7 +42,7 @@ func TestRepository_GetBlob_Found(t *testing.T) { func TestRepository_GetBlob_NotExist(t *testing.T) { repoPath := filepath.Join(testReposDir, "repo1_bare") - r, err := OpenRepository(t.Context(), repoPath) + r, err := OpenRepositoryLocal(repoPath) assert.NoError(t, err) defer r.Close() @@ -56,7 +56,7 @@ func TestRepository_GetBlob_NotExist(t *testing.T) { func TestRepository_GetBlob_NoId(t *testing.T) { repoPath := filepath.Join(testReposDir, "repo1_bare") - r, err := OpenRepository(t.Context(), repoPath) + r, err := OpenRepositoryLocal(repoPath) assert.NoError(t, err) defer r.Close() diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index aef09a0927..a0a9025f1a 100644 --- a/modules/git/repo_branch.go +++ b/modules/git/repo_branch.go @@ -5,6 +5,8 @@ package git import ( + "context" + "gitea.dev/modules/git/gitcmd" ) @@ -12,13 +14,13 @@ import ( const BranchPrefix = "refs/heads/" // AddRemote adds a new remote to repository. -func (repo *Repository) AddRemote(name, url string, fetch bool) error { +func (repo *Repository) AddRemote(ctx context.Context, name, url string, fetch bool) error { cmd := gitcmd.NewCommand("remote", "add") if fetch { cmd.AddArguments("-f") } _, _, err := cmd.AddDynamicArguments(name, url). WithDir(repo.Path). - RunStdString(repo.Ctx) + RunStdString(ctx) return err } diff --git a/modules/git/repo_branch_gogit.go b/modules/git/repo_branch_gogit.go index 77aecb21eb..a791c72b18 100644 --- a/modules/git/repo_branch_gogit.go +++ b/modules/git/repo_branch_gogit.go @@ -7,6 +7,7 @@ package git import ( + "context" "sort" "strings" @@ -19,7 +20,7 @@ import ( // Unlike the implementation of IsObjectExist in nogogit edition, it does not support short hashes here. // For example, IsObjectExist("153f451") will return false, but it will return true in nogogit edition. // To fix this, the solution could be adding support for short hashes in gogit edition if it's really needed. -func (repo *Repository) IsObjectExist(name string) bool { +func (repo *Repository) IsObjectExist(_ context.Context, name string) bool { if name == "" { return false } @@ -33,7 +34,7 @@ func (repo *Repository) IsObjectExist(name string) bool { // Unlike the implementation of IsObjectExist in nogogit edition, it does not support blob hashes here. // For example, IsObjectExist([existing_blob_hash]) will return false, but it will return true in nogogit edition. // To fix this, the solution could be refusing to support blob hashes in nogogit edition since a blob hash is not a reference. -func (repo *Repository) IsReferenceExist(name string) bool { +func (repo *Repository) IsReferenceExist(_ context.Context, name string) bool { if name == "" { return false } @@ -44,7 +45,7 @@ func (repo *Repository) IsReferenceExist(name string) bool { } // IsBranchExist returns true if given branch exists in current repository. -func (repo *Repository) IsBranchExist(name string) bool { +func (repo *Repository) IsBranchExist(_ context.Context, name string) bool { if name == "" { return false } @@ -60,7 +61,7 @@ func (repo *Repository) IsBranchExist(name string) bool { // Branches are returned with sort of `-committerdate` as the nogogit // implementation. This requires full fetch, sort and then the // skip/limit applies later as gogit returns in undefined order. -func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) { +func (repo *Repository) GetBranchNames(_ context.Context, skip, limit int) ([]string, int, error) { type BranchData struct { name string committerDate int64 @@ -100,7 +101,7 @@ func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) { } // WalkReferences walks all the references from the repository -func (repo *Repository) WalkReferences(arg ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) { +func (repo *Repository) WalkReferences(ctx context.Context, arg ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) { i := 0 var iter storer.ReferenceIter var err error @@ -130,13 +131,13 @@ func (repo *Repository) WalkReferences(arg ObjectType, skip, limit int, walkfn f if limit != 0 && i >= skip+limit { return storer.ErrStop } - return nil + return ctx.Err() }) return i, err } // GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash -func (repo *Repository) GetRefsBySha(sha, prefix string) ([]string, error) { +func (repo *Repository) GetRefsBySha(ctx context.Context, sha, prefix string) ([]string, error) { var revList []string iter, err := repo.gogitRepo.References() if err != nil { @@ -146,7 +147,7 @@ func (repo *Repository) GetRefsBySha(sha, prefix string) ([]string, error) { if ref.Hash().String() == sha && strings.HasPrefix(string(ref.Name()), prefix) { revList = append(revList, string(ref.Name())) } - return nil + return ctx.Err() }) return revList, err } diff --git a/modules/git/repo_branch_nogogit.go b/modules/git/repo_branch_nogogit.go index b43b622d05..47f320d800 100644 --- a/modules/git/repo_branch_nogogit.go +++ b/modules/git/repo_branch_nogogit.go @@ -18,12 +18,12 @@ import ( // IsObjectExist returns true if the given object exists in the repository. // FIXME: this function doesn't seem right, it is only used by GarbageCollectLFSMetaObjectsForRepo -func (repo *Repository) IsObjectExist(name string) bool { +func (repo *Repository) IsObjectExist(ctx context.Context, name string) bool { if name == "" { return false } - batch, cancel, err := repo.CatFileBatch(repo.Ctx) + batch, cancel, err := repo.CatFileBatch(ctx) if err != nil { log.Debug("Error opening CatFileBatch %v", err) return false @@ -38,12 +38,12 @@ func (repo *Repository) IsObjectExist(name string) bool { } // IsReferenceExist returns true if given reference exists in the repository. -func (repo *Repository) IsReferenceExist(name string) bool { +func (repo *Repository) IsReferenceExist(ctx context.Context, name string) bool { if name == "" { return false } - batch, cancel, err := repo.CatFileBatch(repo.Ctx) + batch, cancel, err := repo.CatFileBatch(ctx) if err != nil { log.Error("Error opening CatFileBatch %v", err) return false @@ -54,23 +54,23 @@ func (repo *Repository) IsReferenceExist(name string) bool { } // IsBranchExist returns true if given branch exists in current repository. -func (repo *Repository) IsBranchExist(name string) bool { +func (repo *Repository) IsBranchExist(ctx context.Context, name string) bool { if repo == nil || name == "" { return false } - return repo.IsReferenceExist(BranchPrefix + name) + return repo.IsReferenceExist(ctx, BranchPrefix+name) } // GetBranchNames returns branches from the repository, skipping "skip" initial branches and // returning at most "limit" branches, or all branches if "limit" is 0. -func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) { - return callShowRef(repo.Ctx, repo.Path, BranchPrefix, gitcmd.TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}, skip, limit) +func (repo *Repository) GetBranchNames(ctx context.Context, skip, limit int) ([]string, int, error) { + return callShowRef(ctx, repo.Path, BranchPrefix, gitcmd.TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"}, skip, limit) } // WalkReferences walks all the references from the repository // refType should be empty, ObjectTag or ObjectBranch. All other values are equivalent to empty. -func (repo *Repository) WalkReferences(refType ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) { +func (repo *Repository) WalkReferences(ctx context.Context, refType ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) { var args gitcmd.TrustedCmdArgs switch refType { case ObjectTag: @@ -79,7 +79,7 @@ func (repo *Repository) WalkReferences(refType ObjectType, skip, limit int, walk args = gitcmd.TrustedCmdArgs{BranchPrefix, "--sort=-committerdate"} } - return WalkShowRef(repo.Ctx, repo.Path, args, skip, limit, walkfn) + return WalkShowRef(ctx, repo.Path, args, skip, limit, walkfn) } // callShowRef return refs, if limit = 0 it will not limit @@ -172,9 +172,9 @@ func WalkShowRef(ctx context.Context, repoPath string, extraArgs gitcmd.TrustedC } // GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash -func (repo *Repository) GetRefsBySha(sha, prefix string) ([]string, error) { +func (repo *Repository) GetRefsBySha(ctx context.Context, sha, prefix string) ([]string, error) { var revList []string - _, err := WalkShowRef(repo.Ctx, repo.Path, nil, 0, 0, func(walkSha, refname string) error { + _, err := WalkShowRef(ctx, repo.Path, nil, 0, 0, func(walkSha, refname string) error { if walkSha == sha && strings.HasPrefix(refname, prefix) { revList = append(revList, refname) } diff --git a/modules/git/repo_branch_test.go b/modules/git/repo_branch_test.go index 5d586954db..695a1cf068 100644 --- a/modules/git/repo_branch_test.go +++ b/modules/git/repo_branch_test.go @@ -13,25 +13,25 @@ import ( func TestRepository_GetBranches(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) + bareRepo1, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer bareRepo1.Close() - branches, countAll, err := bareRepo1.GetBranchNames(0, 2) + branches, countAll, err := bareRepo1.GetBranchNames(t.Context(), 0, 2) assert.NoError(t, err) assert.Len(t, branches, 2) assert.Equal(t, 3, countAll) assert.ElementsMatch(t, []string{"master", "branch2"}, branches) - branches, countAll, err = bareRepo1.GetBranchNames(0, 0) + branches, countAll, err = bareRepo1.GetBranchNames(t.Context(), 0, 0) assert.NoError(t, err) assert.Len(t, branches, 3) assert.Equal(t, 3, countAll) assert.ElementsMatch(t, []string{"master", "branch2", "branch1"}, branches) - branches, countAll, err = bareRepo1.GetBranchNames(5, 1) + branches, countAll, err = bareRepo1.GetBranchNames(t.Context(), 5, 1) assert.NoError(t, err) assert.Empty(t, branches) @@ -41,14 +41,14 @@ func TestRepository_GetBranches(t *testing.T) { func BenchmarkRepository_GetBranches(b *testing.B) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(b.Context(), bareRepo1Path) + bareRepo1, err := OpenRepositoryLocal(bareRepo1Path) if err != nil { b.Fatal(err) } defer bareRepo1.Close() for b.Loop() { - _, _, err := bareRepo1.GetBranchNames(0, 0) + _, _, err := bareRepo1.GetBranchNames(b.Context(), 0, 0) if err != nil { b.Fatal(err) } @@ -57,47 +57,48 @@ func BenchmarkRepository_GetBranches(b *testing.B) { func TestGetRefsBySha(t *testing.T) { bareRepo5Path := filepath.Join(testReposDir, "repo5_pulls") - bareRepo5, err := OpenRepository(t.Context(), bareRepo5Path) + bareRepo5, err := OpenRepositoryLocal(bareRepo5Path) if err != nil { t.Fatal(err) } defer bareRepo5.Close() // do not exist - branches, err := bareRepo5.GetRefsBySha("8006ff9adbf0cb94da7dad9e537e53817f9fa5c0", "") + branches, err := bareRepo5.GetRefsBySha(t.Context(), "8006ff9adbf0cb94da7dad9e537e53817f9fa5c0", "") assert.NoError(t, err) assert.Empty(t, branches) // refs/pull/1/head - branches, err = bareRepo5.GetRefsBySha("c83380d7056593c51a699d12b9c00627bd5743e9", PullPrefix) + branches, err = bareRepo5.GetRefsBySha(t.Context(), "c83380d7056593c51a699d12b9c00627bd5743e9", PullPrefix) assert.NoError(t, err) assert.Equal(t, []string{"refs/pull/1/head"}, branches) - branches, err = bareRepo5.GetRefsBySha("d8e0bbb45f200e67d9a784ce55bd90821af45ebd", BranchPrefix) + branches, err = bareRepo5.GetRefsBySha(t.Context(), "d8e0bbb45f200e67d9a784ce55bd90821af45ebd", BranchPrefix) assert.NoError(t, err) assert.Equal(t, []string{"refs/heads/master", "refs/heads/master-clone"}, branches) - branches, err = bareRepo5.GetRefsBySha("58a4bcc53ac13e7ff76127e0fb518b5262bf09af", BranchPrefix) + branches, err = bareRepo5.GetRefsBySha(t.Context(), "58a4bcc53ac13e7ff76127e0fb518b5262bf09af", BranchPrefix) assert.NoError(t, err) assert.Equal(t, []string{"refs/heads/test-patch-1"}, branches) } func BenchmarkGetRefsBySha(b *testing.B) { bareRepo5Path := filepath.Join(testReposDir, "repo5_pulls") - bareRepo5, err := OpenRepository(b.Context(), bareRepo5Path) + bareRepo5, err := OpenRepositoryLocal(bareRepo5Path) if err != nil { b.Fatal(err) } defer bareRepo5.Close() - _, _ = bareRepo5.GetRefsBySha("8006ff9adbf0cb94da7dad9e537e53817f9fa5c0", "") - _, _ = bareRepo5.GetRefsBySha("d8e0bbb45f200e67d9a784ce55bd90821af45ebd", "") - _, _ = bareRepo5.GetRefsBySha("c83380d7056593c51a699d12b9c00627bd5743e9", "") - _, _ = bareRepo5.GetRefsBySha("58a4bcc53ac13e7ff76127e0fb518b5262bf09af", "") + _, _ = bareRepo5.GetRefsBySha(b.Context(), "8006ff9adbf0cb94da7dad9e537e53817f9fa5c0", "") + _, _ = bareRepo5.GetRefsBySha(b.Context(), "d8e0bbb45f200e67d9a784ce55bd90821af45ebd", "") + _, _ = bareRepo5.GetRefsBySha(b.Context(), "c83380d7056593c51a699d12b9c00627bd5743e9", "") + _, _ = bareRepo5.GetRefsBySha(b.Context(), "58a4bcc53ac13e7ff76127e0fb518b5262bf09af", "") } func TestRepository_IsObjectExist(t *testing.T) { - repo, err := OpenRepository(t.Context(), filepath.Join(testReposDir, "repo1_bare")) + ctx := t.Context() + repo, err := OpenRepositoryLocal(filepath.Join(testReposDir, "repo1_bare")) require.NoError(t, err) defer repo.Close() @@ -143,13 +144,14 @@ func TestRepository_IsObjectExist(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, repo.IsObjectExist(tt.arg)) + assert.Equal(t, tt.want, repo.IsObjectExist(ctx, tt.arg)) }) } } func TestRepository_IsReferenceExist(t *testing.T) { - repo, err := OpenRepository(t.Context(), filepath.Join(testReposDir, "repo1_bare")) + ctx := t.Context() + repo, err := OpenRepositoryLocal(filepath.Join(testReposDir, "repo1_bare")) require.NoError(t, err) defer repo.Close() @@ -195,7 +197,7 @@ func TestRepository_IsReferenceExist(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, repo.IsReferenceExist(tt.arg)) + assert.Equal(t, tt.want, repo.IsReferenceExist(ctx, tt.arg)) }) } } diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 1a93504d97..6397700272 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -6,6 +6,7 @@ package git import ( "bytes" + "context" "io" "strconv" "strings" @@ -15,36 +16,68 @@ import ( ) // GetBranchCommitID returns last commit ID string of given branch. -func (repo *Repository) GetBranchCommitID(name string) (string, error) { - return repo.GetRefCommitID(BranchPrefix + name) +func (repo *Repository) GetBranchCommitID(ctx context.Context, name string) (string, error) { + return repo.GetRefCommitID(ctx, BranchPrefix+name) } // GetTagCommitID returns last commit ID string of given tag. -func (repo *Repository) GetTagCommitID(name string) (string, error) { - return repo.GetRefCommitID(TagPrefix + name) +func (repo *Repository) GetTagCommitID(ctx context.Context, name string) (string, error) { + return repo.GetRefCommitID(ctx, TagPrefix+name) } // GetCommit returns a commit object of by the git ref. -func (repo *Repository) GetCommit(ref string) (*Commit, error) { - id, err := repo.ConvertToGitID(ref) +func (repo *Repository) GetCommit(ctx context.Context, ref string) (*Commit, error) { + id, err := repo.ConvertToGitID(ctx, ref) if err != nil { return nil, err } - return repo.getCommit(id) + return repo.getCommit(ctx, id) } // GetBranchCommit returns the last commit of given branch. -func (repo *Repository) GetBranchCommit(name string) (*Commit, error) { - return repo.GetCommit(RefNameFromBranch(name).String()) +func (repo *Repository) GetBranchCommit(ctx context.Context, name string) (*Commit, error) { + return repo.GetCommit(ctx, RefNameFromBranch(name).String()) } // GetTagCommit get the commit of the specific tag via name -func (repo *Repository) GetTagCommit(name string) (*Commit, error) { - return repo.GetCommit(RefNameFromTag(name).String()) +func (repo *Repository) GetTagCommit(ctx context.Context, name string) (*Commit, error) { + return repo.GetCommit(ctx, RefNameFromTag(name).String()) } -func (repo *Repository) getCommitByPathWithID(id ObjectID, relpath string) (*Commit, error) { +const prettyLogFormat = `--pretty=format:%H` + +func (repo *Repository) ShowPrettyFormatLogToList(ctx context.Context, revisionRange string) ([]*Commit, error) { + // avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git [...] -- [...]' + logs, _, err := gitcmd.NewCommand("log").AddArguments(prettyLogFormat). + AddDynamicArguments(revisionRange).AddArguments("--").WithDir(repo.Path). + RunStdBytes(ctx) + if err != nil { + return nil, err + } + return repo.parsePrettyFormatLogToList(ctx, logs) +} + +func (repo *Repository) parsePrettyFormatLogToList(ctx context.Context, logs []byte) ([]*Commit, error) { + var commits []*Commit + if len(logs) == 0 { + return commits, nil + } + + parts := bytes.SplitSeq(logs, []byte{'\n'}) + + for commitID := range parts { + commit, err := repo.GetCommit(ctx, string(commitID)) + if err != nil { + return nil, err + } + commits = append(commits, commit) + } + + return commits, nil +} + +func (repo *Repository) getCommitByPathWithID(ctx context.Context, id ObjectID, relpath string) (*Commit, error) { // File name starts with ':' must be escaped. if strings.HasPrefix(relpath, ":") { relpath = `\` + relpath @@ -54,7 +87,7 @@ func (repo *Repository) getCommitByPathWithID(id ObjectID, relpath string) (*Com AddDynamicArguments(id.String()). AddDashesAndList(relpath). WithDir(repo.Path). - RunStdString(repo.Ctx) + RunStdString(ctx) if runErr != nil { return nil, runErr } @@ -64,20 +97,20 @@ func (repo *Repository) getCommitByPathWithID(id ObjectID, relpath string) (*Com return nil, err } - return repo.getCommit(id) + return repo.getCommit(ctx, id) } // GetCommitByPath returns the last commit of relative path. -func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) { +func (repo *Repository) GetCommitByPath(ctx context.Context, relpath string) (*Commit, error) { stdout, _, runErr := gitcmd.NewCommand("log", "-1", prettyLogFormat). AddDashesAndList(relpath). WithDir(repo.Path). - RunStdBytes(repo.Ctx) + RunStdBytes(ctx) if runErr != nil { return nil, runErr } - commits, err := repo.parsePrettyFormatLogToList(stdout) + commits, err := repo.parsePrettyFormatLogToList(ctx, stdout) if err != nil { return nil, err } @@ -88,7 +121,7 @@ func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) { } // commitsByRangeWithTime returns the specific page commits before current revision, with not, since, until support -func (repo *Repository) commitsByRangeWithTime(id ObjectID, page, pageSize int, not, since, until string) ([]*Commit, error) { +func (repo *Repository) commitsByRangeWithTime(ctx context.Context, id ObjectID, page, pageSize int, not, since, until string) ([]*Commit, error) { cmd := gitcmd.NewCommand("log"). AddOptionFormat("--skip=%d", (page-1)*pageSize). AddOptionFormat("--max-count=%d", pageSize). @@ -105,15 +138,15 @@ func (repo *Repository) commitsByRangeWithTime(id ObjectID, page, pageSize int, cmd.AddOptionFormat("--until=%s", until) } - stdout, _, err := cmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) + stdout, _, err := cmd.WithDir(repo.Path).RunStdBytes(ctx) if err != nil { return nil, err } - return repo.parsePrettyFormatLogToList(stdout) + return repo.parsePrettyFormatLogToList(ctx, stdout) } -func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([]*Commit, error) { +func (repo *Repository) searchCommits(ctx context.Context, id ObjectID, opts SearchCommitsOptions) ([]*Commit, error) { // add common arguments to git command addCommonSearchArgs := func(c *gitcmd.Command) { // ignore case @@ -159,7 +192,7 @@ func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([ // search for commits matching given constraints and keywords in commit msg addCommonSearchArgs(cmd) - stdout, _, err := cmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) + stdout, _, err := cmd.WithDir(repo.Path).RunStdBytes(ctx) if err != nil { return nil, err } @@ -180,7 +213,7 @@ func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([ hashCmd.AddDynamicArguments(v) // search with given constraints for commit matching sha hash of v - hashMatching, _, err := hashCmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) + hashMatching, _, err := hashCmd.WithDir(repo.Path).RunStdBytes(ctx) if err != nil || bytes.Contains(stdout, hashMatching) { continue } @@ -189,17 +222,17 @@ func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([ } } - return repo.parsePrettyFormatLogToList(bytes.TrimSuffix(stdout, []byte{'\n'})) + return repo.parsePrettyFormatLogToList(ctx, bytes.TrimSuffix(stdout, []byte{'\n'})) } // FileChangedBetweenCommits Returns true if the file changed between commit IDs id1 and id2 // You must ensure that id1 and id2 are valid commit ids. -func (repo *Repository) FileChangedBetweenCommits(filename, id1, id2 string) (bool, error) { +func (repo *Repository) FileChangedBetweenCommits(ctx context.Context, filename, id1, id2 string) (bool, error) { stdout, _, err := gitcmd.NewCommand("diff", "--name-only", "-z"). AddDynamicArguments(id1, id2). AddDashesAndList(filename). WithDir(repo.Path). - RunStdBytes(repo.Ctx) + RunStdBytes(ctx) if err != nil { return false, err } @@ -219,7 +252,7 @@ type CommitsByFileAndRangeOptions struct { } // CommitsByFileAndRange return the commits according revision file and the page -func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) (commits []*Commit, hasMore bool, _ error) { +func (repo *Repository) CommitsByFileAndRange(ctx context.Context, opts CommitsByFileAndRangeOptions) (commits []*Commit, hasMore bool, _ error) { limit := setting.Git.CommitsRangeSize gitCmd := gitcmd.NewCommand("--no-pager", "log"). AddArguments("--pretty=tformat:%H"). @@ -244,7 +277,7 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) defer stdoutReaderClose() err := gitCmd.WithDir(repo.Path). WithPipelineFunc(func(context gitcmd.Context) error { - objectFormat, err := repo.GetObjectFormat() + objectFormat, err := repo.GetObjectFormat(ctx) if err != nil { return err } @@ -263,14 +296,14 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) if err != nil { return err } - commit, err := repo.getCommit(objectID) + commit, err := repo.getCommit(ctx, objectID) if err != nil { return err } commits = append(commits, commit) } }). - RunWithStderr(repo.Ctx) + RunWithStderr(ctx) hasMore = len(commits) > limit if hasMore { @@ -281,7 +314,7 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) // CommitsBetween returns a list that contains commits between [after, before). After is the first item in the slice. // If "before" and "after" are not related, it returns the all commits for the "after" commit. -func (repo *Repository) CommitsBetween(afterRef, beforeRef RefName, limit int, optSkip ...int) ([]*Commit, error) { +func (repo *Repository) CommitsBetween(ctx context.Context, afterRef, beforeRef RefName, limit int, optSkip ...int) ([]*Commit, error) { gitCmd := func() *gitcmd.Command { cmd := gitcmd.NewCommand("rev-list").WithDir(repo.Path) if limit >= 0 { @@ -295,42 +328,42 @@ func (repo *Repository) CommitsBetween(afterRef, beforeRef RefName, limit int, o var stdout []byte var err error if beforeRef == "" { - stdout, _, err = gitCmd().AddDynamicArguments(afterRef.String()).RunStdBytes(repo.Ctx) + stdout, _, err = gitCmd().AddDynamicArguments(afterRef.String()).RunStdBytes(ctx) } else { - stdout, _, err = gitCmd().AddDynamicArguments(beforeRef.String() + ".." + afterRef.String()).RunStdBytes(repo.Ctx) + stdout, _, err = gitCmd().AddDynamicArguments(beforeRef.String() + ".." + afterRef.String()).RunStdBytes(ctx) if gitcmd.IsStderr(err, gitcmd.StderrNoMergeBase) { // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. // if the beforeRef and afterRef are not related (no merge base), just get all commits pushed by afterRef - stdout, _, err = gitCmd().AddDynamicArguments(afterRef.String()).RunStdBytes(repo.Ctx) + stdout, _, err = gitCmd().AddDynamicArguments(afterRef.String()).RunStdBytes(ctx) } } if err != nil { return nil, err } - return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout)) + return repo.parsePrettyFormatLogToList(ctx, bytes.TrimSpace(stdout)) } // commitsBefore the limit is depth, not total number of returned commits. -func (repo *Repository) commitsBefore(id ObjectID, limit int) ([]*Commit, error) { +func (repo *Repository) commitsBefore(ctx context.Context, id ObjectID, limit int) ([]*Commit, error) { cmd := gitcmd.NewCommand("log", prettyLogFormat) if limit > 0 { cmd.AddOptionFormat("-%d", limit) } cmd.AddDynamicArguments(id.String()) - stdout, _, runErr := cmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) + stdout, _, runErr := cmd.WithDir(repo.Path).RunStdBytes(ctx) if runErr != nil { return nil, runErr } - formattedLog, err := repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout)) + formattedLog, err := repo.parsePrettyFormatLogToList(ctx, bytes.TrimSpace(stdout)) if err != nil { return nil, err } commits := make([]*Commit, 0, len(formattedLog)) for _, commit := range formattedLog { - branches, err := repo.getBranches(nil, commit.ID.String(), 2) + branches, err := repo.getBranches(ctx, nil, commit.ID.String(), 2) if err != nil { return nil, err } @@ -345,22 +378,22 @@ func (repo *Repository) commitsBefore(id ObjectID, limit int) ([]*Commit, error) return commits, nil } -func (repo *Repository) getCommitsBefore(id ObjectID) ([]*Commit, error) { - return repo.commitsBefore(id, 0) +func (repo *Repository) getCommitsBefore(ctx context.Context, id ObjectID) ([]*Commit, error) { + return repo.commitsBefore(ctx, id, 0) } -func (repo *Repository) getCommitsBeforeLimit(id ObjectID, num int) ([]*Commit, error) { - return repo.commitsBefore(id, num) +func (repo *Repository) getCommitsBeforeLimit(ctx context.Context, id ObjectID, num int) ([]*Commit, error) { + return repo.commitsBefore(ctx, id, num) } -func (repo *Repository) getBranches(env []string, commitID string, limit int) ([]string, error) { +func (repo *Repository) getBranches(ctx context.Context, env []string, commitID string, limit int) ([]string, error) { stdout, _, err := gitcmd.NewCommand("for-each-ref", "--format=%(refname:strip=2)"). AddOptionFormat("--count=%d", limit). AddOptionValues("--contains", commitID). AddArguments(BranchPrefix). WithEnv(env). WithDir(repo.Path). - RunStdString(repo.Ctx) + RunStdString(ctx) if err != nil { return nil, err } @@ -368,11 +401,11 @@ func (repo *Repository) getBranches(env []string, commitID string, limit int) ([ } // GetCommitsFromIDs get commits from commit IDs -func (repo *Repository) GetCommitsFromIDs(commitIDs []string) []*Commit { +func (repo *Repository) GetCommitsFromIDs(ctx context.Context, commitIDs []string) []*Commit { commits := make([]*Commit, 0, len(commitIDs)) for _, commitID := range commitIDs { - commit, err := repo.GetCommit(commitID) + commit, err := repo.GetCommit(ctx, commitID) if err == nil && commit != nil { commits = append(commits, commit) } @@ -382,11 +415,11 @@ func (repo *Repository) GetCommitsFromIDs(commitIDs []string) []*Commit { } // IsCommitInBranch check if the commit is on the branch -func (repo *Repository) IsCommitInBranch(commitID, branch string) (r bool, err error) { +func (repo *Repository) IsCommitInBranch(ctx context.Context, commitID, branch string) (r bool, err error) { stdout, _, err := gitcmd.NewCommand("branch", "--contains"). AddDynamicArguments(commitID, branch). WithDir(repo.Path). - RunStdString(repo.Ctx) + RunStdString(ctx) if err != nil { return false, err } @@ -394,13 +427,13 @@ func (repo *Repository) IsCommitInBranch(commitID, branch string) (r bool, err e } // GetCommitBranchStart returns the commit where the branch diverged -func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID string) (string, error) { +func (repo *Repository) GetCommitBranchStart(ctx context.Context, env []string, branch, endCommitID string) (string, error) { cmd := gitcmd.NewCommand("log", prettyLogFormat) cmd.AddDynamicArguments(endCommitID) stdout, _, runErr := cmd.WithDir(repo.Path). WithEnv(env). - RunStdBytes(repo.Ctx) + RunStdBytes(ctx) if runErr != nil { return "", runErr } @@ -411,7 +444,7 @@ func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID s // and we think this commit is the divergence point for part := range parts { commitID := string(part) - branches, err := repo.getBranches(env, commitID, 2) + branches, err := repo.getBranches(ctx, env, commitID, 2) if err != nil { return "", err } diff --git a/modules/git/repo_commit_gogit.go b/modules/git/repo_commit_gogit.go index a7e9731b15..551b668165 100644 --- a/modules/git/repo_commit_gogit.go +++ b/modules/git/repo_commit_gogit.go @@ -7,6 +7,7 @@ package git import ( + "context" "strings" "gitea.dev/modules/git/gitcmd" @@ -17,7 +18,7 @@ import ( ) // GetRefCommitID returns the last commit ID string of given reference. -func (repo *Repository) GetRefCommitID(name string) (string, error) { +func (repo *Repository) GetRefCommitID(_ context.Context, name string) (string, error) { if plumbing.IsHash(name) { return name, nil } @@ -42,8 +43,8 @@ func (repo *Repository) GetRefCommitID(name string) (string, error) { } // ConvertToHash returns a Hash object from a potential ID string -func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { - objectFormat, err := repo.GetObjectFormat() +func (repo *Repository) ConvertToGitID(ctx context.Context, commitID string) (ObjectID, error) { + objectFormat, err := repo.GetObjectFormat(ctx) if err != nil { return nil, err } @@ -57,7 +58,7 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { actualCommitID, _, err := gitcmd.NewCommand("rev-parse", "--verify"). AddDynamicArguments(commitID). WithDir(repo.Path). - RunStdString(repo.Ctx) + RunStdString(ctx) actualCommitID = strings.TrimSpace(actualCommitID) if err != nil { if strings.Contains(err.Error(), "unknown revision or path") || @@ -70,7 +71,7 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { return NewIDFromString(actualCommitID) } -func (repo *Repository) getCommit(id ObjectID) (*Commit, error) { +func (repo *Repository) getCommit(_ context.Context, id ObjectID) (*Commit, error) { var tagObject *object.Tag commitID := plumbing.Hash(id.RawValue()) @@ -92,15 +93,14 @@ func (repo *Repository) getCommit(id ObjectID) (*Commit, error) { } commit := convertCommit(gogitCommit) - commit.repo = repo tree, err := gogitCommit.Tree() if err != nil { return nil, err } - commit.Tree.ID = ParseGogitHash(tree.Hash) - commit.Tree.resolvedGogitTreeObject = tree + commit.TreeID = ParseGogitHash(tree.Hash) + commit.Tree().resolvedGogitTreeObject = tree return commit, nil } diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go index d8b842beee..50eaabfe22 100644 --- a/modules/git/repo_commit_nogogit.go +++ b/modules/git/repo_commit_nogogit.go @@ -6,6 +6,7 @@ package git import ( + "context" "errors" "io" "strings" @@ -15,11 +16,11 @@ import ( ) // ResolveReference resolves a name to a reference -func (repo *Repository) ResolveReference(name string) (string, error) { +func (repo *Repository) ResolveReference(ctx context.Context, name string) (string, error) { stdout, _, err := gitcmd.NewCommand("show-ref", "--hash"). AddDynamicArguments(name). WithDir(repo.Path). - RunStdString(repo.Ctx) + RunStdString(ctx) if err != nil { if strings.Contains(err.Error(), "not a valid ref") { return "", ErrNotExist{name, ""} @@ -35,8 +36,8 @@ func (repo *Repository) ResolveReference(name string) (string, error) { } // GetRefCommitID returns the last commit ID string of given reference (branch or tag). -func (repo *Repository) GetRefCommitID(name string) (string, error) { - batch, cancel, err := repo.CatFileBatch(repo.Ctx) +func (repo *Repository) GetRefCommitID(ctx context.Context, name string) (string, error) { + batch, cancel, err := repo.CatFileBatch(ctx) if err != nil { return "", err } @@ -50,8 +51,8 @@ func (repo *Repository) GetRefCommitID(name string) (string, error) { return info.ID, nil } -func (repo *Repository) getCommit(id ObjectID) (*Commit, error) { - batch, cancel, err := repo.CatFileBatch(repo.Ctx) +func (repo *Repository) getCommit(ctx context.Context, id ObjectID) (*Commit, error) { + batch, cancel, err := repo.CatFileBatch(ctx) if err != nil { return nil, err } @@ -88,7 +89,7 @@ func (repo *Repository) getCommitWithBatch(batch CatFileBatch, id ObjectID) (*Co } return repo.getCommitWithBatch(batch, tag.Object) case "commit": - commit, err := CommitFromReader(repo, id, io.LimitReader(rd, info.Size)) + commit, err := CommitFromReader(id, io.LimitReader(rd, info.Size)) if err != nil { return nil, err } @@ -110,8 +111,8 @@ func (repo *Repository) getCommitWithBatch(batch CatFileBatch, id ObjectID) (*Co } // ConvertToGitID returns a git object ID from the git ref, it doesn't guarantee the returned ID really exists -func (repo *Repository) ConvertToGitID(ref string) (ObjectID, error) { - objectFormat, err := repo.GetObjectFormat() +func (repo *Repository) ConvertToGitID(ctx context.Context, ref string) (ObjectID, error) { + objectFormat, err := repo.GetObjectFormat(ctx) if err != nil { return nil, err } @@ -122,7 +123,7 @@ func (repo *Repository) ConvertToGitID(ref string) (ObjectID, error) { } } - batch, cancel, err := repo.CatFileBatch(repo.Ctx) + batch, cancel, err := repo.CatFileBatch(ctx) if err != nil { return nil, err } diff --git a/modules/git/repo_commit_test.go b/modules/git/repo_commit_test.go index c57dec9503..fb799756cb 100644 --- a/modules/git/repo_commit_test.go +++ b/modules/git/repo_commit_test.go @@ -18,7 +18,7 @@ import ( func TestRepository_GetCommitBranches(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) + bareRepo1, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer bareRepo1.Close() @@ -35,9 +35,9 @@ func TestRepository_GetCommitBranches(t *testing.T) { {"master", []string{"master"}}, } for _, testCase := range testCases { - commit, err := bareRepo1.GetCommit(testCase.CommitID) + commit, err := bareRepo1.GetCommit(t.Context(), testCase.CommitID) assert.NoError(t, err) - branches, err := bareRepo1.getBranches(nil, commit.ID.String(), 2) + branches, err := bareRepo1.getBranches(t.Context(), nil, commit.ID.String(), 2) assert.NoError(t, err) assert.Equal(t, testCase.ExpectedBranches, branches) } @@ -45,12 +45,12 @@ func TestRepository_GetCommitBranches(t *testing.T) { func TestGetTagCommitWithSignature(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) + bareRepo1, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer bareRepo1.Close() // both the tag and the commit are signed here, this validates only the commit signature - commit, err := bareRepo1.GetCommit("28b55526e7100924d864dd89e35c1ea62e7a5a32") + commit, err := bareRepo1.GetCommit(t.Context(), "28b55526e7100924d864dd89e35c1ea62e7a5a32") assert.NoError(t, err) assert.NotNil(t, commit) assert.NotNil(t, commit.Signature) @@ -60,11 +60,11 @@ func TestGetTagCommitWithSignature(t *testing.T) { func TestGetCommitWithBadCommitID(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) + bareRepo1, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer bareRepo1.Close() - commit, err := bareRepo1.GetCommit("bad_branch") + commit, err := bareRepo1.GetCommit(t.Context(), "bad_branch") assert.Nil(t, commit) assert.Error(t, err) assert.True(t, IsErrNotExist(err)) @@ -72,22 +72,22 @@ func TestGetCommitWithBadCommitID(t *testing.T) { func TestIsCommitInBranch(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) + bareRepo1, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer bareRepo1.Close() - result, err := bareRepo1.IsCommitInBranch("2839944139e0de9737a044f78b0e4b40d989a9e3", "branch1") + result, err := bareRepo1.IsCommitInBranch(t.Context(), "2839944139e0de9737a044f78b0e4b40d989a9e3", "branch1") assert.NoError(t, err) assert.True(t, result) - result, err = bareRepo1.IsCommitInBranch("2839944139e0de9737a044f78b0e4b40d989a9e3", "branch2") + result, err = bareRepo1.IsCommitInBranch(t.Context(), "2839944139e0de9737a044f78b0e4b40d989a9e3", "branch2") assert.NoError(t, err) assert.False(t, result) } func TestRepository_CommitsBetween(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo4_commitsbetween") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) + bareRepo1, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer bareRepo1.Close() @@ -101,7 +101,7 @@ func TestRepository_CommitsBetween(t *testing.T) { {"78a445db1eac62fe15e624e1137965969addf344", "a78e5638b66ccfe7e1b4689d3d5684e42c97d7ca", 1}, // com2 -> com2_new } for i, c := range cases { - commits, err := bareRepo1.CommitsBetween(c.NewID, c.OldID, -1) + commits, err := bareRepo1.CommitsBetween(t.Context(), c.NewID, c.OldID, -1) assert.NoError(t, err) assert.Len(t, commits, c.ExpectedCommits, "case %d", i) } @@ -109,7 +109,7 @@ func TestRepository_CommitsBetween(t *testing.T) { func TestGetRefCommitID(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) + bareRepo1, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer bareRepo1.Close() @@ -125,7 +125,7 @@ func TestGetRefCommitID(t *testing.T) { } for _, testCase := range testCases { - commitID, err := bareRepo1.GetRefCommitID(testCase.Ref) + commitID, err := bareRepo1.GetRefCommitID(t.Context(), testCase.Ref) if assert.NoError(t, err) { assert.Equal(t, testCase.ExpectedCommitID, commitID) } @@ -136,17 +136,17 @@ func TestCommitsByFileAndRange(t *testing.T) { defer test.MockVariableValue(&setting.Git.CommitsRangeSize, 2)() bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) + bareRepo1, err := OpenRepositoryLocal(bareRepo1Path) require.NoError(t, err) defer bareRepo1.Close() // "foo" has 3 commits in "master" branch - commits, hasMore, err := bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 1}) + commits, hasMore, err := bareRepo1.CommitsByFileAndRange(t.Context(), CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 1}) require.NoError(t, err) assert.True(t, hasMore) assert.Len(t, commits, 2) - commits, hasMore, err = bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 2}) + commits, hasMore, err = bareRepo1.CommitsByFileAndRange(t.Context(), CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 2}) require.NoError(t, err) assert.Len(t, commits, 1) assert.False(t, hasMore) @@ -179,14 +179,14 @@ M 100644 :1 b.txt `))).RunStdString(t.Context()) require.NoError(t, runErr) - repoFollowRename, err := OpenRepository(t.Context(), repoFollowRenameDir) + repoFollowRename, err := OpenRepositoryLocal(repoFollowRenameDir) require.NoError(t, err) defer repoFollowRename.Close() - commits, _, err = repoFollowRename.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "b.txt", Page: 1}) + commits, _, err = repoFollowRename.CommitsByFileAndRange(t.Context(), CommitsByFileAndRangeOptions{Revision: "master", File: "b.txt", Page: 1}) require.NoError(t, err) assert.Len(t, commits, 1) - commits, _, err = repoFollowRename.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "b.txt", Page: 1, FollowRename: true}) + commits, _, err = repoFollowRename.CommitsByFileAndRange(t.Context(), CommitsByFileAndRangeOptions{Revision: "master", File: "b.txt", Page: 1, FollowRename: true}) require.NoError(t, err) assert.Len(t, commits, 2) } diff --git a/modules/git/repo_commitgraph.go b/modules/git/repo_commitgraph.go index 4db3c70f52..e03cc60213 100644 --- a/modules/git/repo_commitgraph.go +++ b/modules/git/repo_commitgraph.go @@ -12,10 +12,10 @@ import ( // WriteCommitGraph write commit graph to speed up repo access // this requires git v2.18 to be installed -func WriteCommitGraph(ctx context.Context, repoPath string) error { +func WriteCommitGraph(ctx context.Context, repo RepositoryFacade) error { if DefaultFeatures().CheckVersionAtLeast("2.18") { - if _, _, err := gitcmd.NewCommand("commit-graph", "write").WithDir(repoPath).RunStdString(ctx); err != nil { - return fmt.Errorf("unable to write commit-graph for '%s' : %w", repoPath, err) + if _, _, err := gitcmd.NewCommand("commit-graph", "write").WithRepo(repo).RunStdString(ctx); err != nil { + return fmt.Errorf("unable to write commit-graph for '%s' : %w", repo.GitRepoLocation(), err) } } return nil diff --git a/modules/git/repo_compare.go b/modules/git/repo_compare.go index 1754a19b4b..6c268b52a7 100644 --- a/modules/git/repo_compare.go +++ b/modules/git/repo_compare.go @@ -7,6 +7,7 @@ package git import ( "bufio" "bytes" + "context" "errors" "fmt" "io" @@ -31,7 +32,7 @@ func (l *lineCountWriter) Write(p []byte) (n int, err error) { // GetDiffNumChangedFiles counts the number of changed files // This is substantially quicker than shortstat but... -func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparison bool) (int, error) { +func (repo *Repository) GetDiffNumChangedFiles(ctx context.Context, base, head string, directComparison bool) (int, error) { // Now there is git diff --shortstat but this appears to be slower than simply iterating with --nameonly w := &lineCountWriter{} @@ -45,7 +46,7 @@ func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparis AddArguments("--"). WithDir(repo.Path). WithStdoutCopy(w). - RunWithStderr(repo.Ctx); err != nil { + RunWithStderr(ctx); err != nil { if gitcmd.IsStderr(err, gitcmd.StderrNoMergeBase) { // git >= 2.28 now returns an error if base and head have become unrelated. // it doesn't make sense to count the changed files in this case because UI won't display such diff @@ -59,35 +60,35 @@ func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparis var patchCommits = regexp.MustCompile(`^From\s(\w+)\s`) // GetDiff generates and returns patch data between given revisions, optimized for human readability -func (repo *Repository) GetDiff(compareArg string, w io.Writer) error { +func (repo *Repository) GetDiff(ctx context.Context, compareArg string, w io.Writer) error { return gitcmd.NewCommand("diff", "-p").AddDynamicArguments(compareArg). WithDir(repo.Path). WithStdoutCopy(w). - Run(repo.Ctx) + Run(ctx) } // GetDiffBinary generates and returns patch data between given revisions, including binary diffs. -func (repo *Repository) GetDiffBinary(compareArg string, w io.Writer) error { +func (repo *Repository) GetDiffBinary(ctx context.Context, compareArg string, w io.Writer) error { return gitcmd.NewCommand("diff", "-p", "--binary", "--histogram"). AddDynamicArguments(compareArg). WithDir(repo.Path). WithStdoutCopy(w). - Run(repo.Ctx) + Run(ctx) } // GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply` -func (repo *Repository) GetPatch(compareArg string, w io.Writer) error { +func (repo *Repository) GetPatch(ctx context.Context, compareArg string, w io.Writer) error { return gitcmd.NewCommand("format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg). WithDir(repo.Path). WithStdoutCopy(w). - Run(repo.Ctx) + Run(ctx) } // GetFilesChangedBetween returns a list of all files that have been changed between the given commits // If base is undefined empty SHA (zeros), it only returns the files changed in the head commit // If base is the SHA of an empty tree (EmptyTreeSHA), it returns the files changes from the initial commit to the head commit -func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, error) { - objectFormat, err := repo.GetObjectFormat() +func (repo *Repository) GetFilesChangedBetween(ctx context.Context, base, head string) ([]string, error) { + objectFormat, err := repo.GetObjectFormat(ctx) if err != nil { return nil, err } @@ -97,7 +98,7 @@ func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, err } else { cmd.AddDynamicArguments(base, head) } - stdout, _, err := cmd.WithDir(repo.Path).RunStdString(repo.Ctx) + stdout, _, err := cmd.WithDir(repo.Path).RunStdString(ctx) if err != nil { return nil, err } diff --git a/modules/git/repo_compare_test.go b/modules/git/repo_compare_test.go index 21c1e2a432..07cc0ffcd3 100644 --- a/modules/git/repo_compare_test.go +++ b/modules/git/repo_compare_test.go @@ -22,7 +22,7 @@ func TestGetFormatPatch(t *testing.T) { return } - repo, err := OpenRepository(t.Context(), clonedPath) + repo, err := OpenRepositoryLocal(clonedPath) if err != nil { assert.NoError(t, err) return @@ -30,7 +30,7 @@ func TestGetFormatPatch(t *testing.T) { defer repo.Close() rd := &bytes.Buffer{} - err = repo.GetPatch("8d92fc95^...8d92fc95", rd) + err = repo.GetPatch(t.Context(), "8d92fc95^...8d92fc95", rd) if err != nil { assert.NoError(t, err) return @@ -50,7 +50,7 @@ func TestGetFormatPatch(t *testing.T) { func TestReadPatch(t *testing.T) { // Ensure we can read the patch files bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - repo, err := OpenRepository(t.Context(), bareRepo1Path) + repo, err := OpenRepositoryLocal(bareRepo1Path) if err != nil { assert.NoError(t, err) return @@ -88,7 +88,7 @@ func TestReadWritePullHead(t *testing.T) { return } - repo, err := OpenRepository(t.Context(), clonedPath) + repo, err := OpenRepositoryLocal(clonedPath) if err != nil { assert.NoError(t, err) return @@ -96,7 +96,7 @@ func TestReadWritePullHead(t *testing.T) { defer repo.Close() // Try to open non-existing Pull - _, err = repo.GetRefCommitID(PullPrefix + "0/head") + _, err = repo.GetRefCommitID(t.Context(), PullPrefix+"0/head") assert.Error(t, err) // Write a fake sha1 with only 40 zeros @@ -111,7 +111,7 @@ func TestReadWritePullHead(t *testing.T) { } // Read the file created - headContents, err := repo.GetRefCommitID(PullPrefix + "1/head") + headContents, err := repo.GetRefCommitID(t.Context(), PullPrefix+"1/head") if err != nil { assert.NoError(t, err) return @@ -130,11 +130,11 @@ func TestReadWritePullHead(t *testing.T) { func TestGetCommitFilesChanged(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - repo, err := OpenRepository(t.Context(), bareRepo1Path) + repo, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer repo.Close() - objectFormat, err := repo.GetObjectFormat() + objectFormat, err := repo.GetObjectFormat(t.Context()) assert.NoError(t, err) testCases := []struct { @@ -164,7 +164,7 @@ func TestGetCommitFilesChanged(t *testing.T) { } for _, tc := range testCases { - changedFiles, err := repo.GetFilesChangedBetween(tc.base, tc.head) + changedFiles, err := repo.GetFilesChangedBetween(t.Context(), tc.base, tc.head) assert.NoError(t, err) assert.ElementsMatch(t, tc.files, changedFiles) } diff --git a/modules/git/repo_index.go b/modules/git/repo_index.go index 957bb6ee32..78c27514f5 100644 --- a/modules/git/repo_index.go +++ b/modules/git/repo_index.go @@ -15,14 +15,14 @@ import ( ) // ReadTreeToIndex reads a treeish to the index -func (repo *Repository) ReadTreeToIndex(treeish string, indexFilename ...string) error { - objectFormat, err := repo.GetObjectFormat() +func (repo *Repository) ReadTreeToIndex(ctx context.Context, treeish string, indexFilename ...string) error { + objectFormat, err := repo.GetObjectFormat(ctx) if err != nil { return err } if len(treeish) != objectFormat.FullLength() { - res, _, err := gitcmd.NewCommand("rev-parse", "--verify").AddDynamicArguments(treeish).WithDir(repo.Path).RunStdString(repo.Ctx) + res, _, err := gitcmd.NewCommand("rev-parse", "--verify").AddDynamicArguments(treeish).WithDir(repo.Path).RunStdString(ctx) if err != nil { return err } @@ -34,15 +34,15 @@ func (repo *Repository) ReadTreeToIndex(treeish string, indexFilename ...string) if err != nil { return err } - return repo.readTreeToIndex(id, indexFilename...) + return repo.readTreeToIndex(ctx, id, indexFilename...) } -func (repo *Repository) readTreeToIndex(id ObjectID, indexFilename ...string) error { +func (repo *Repository) readTreeToIndex(ctx context.Context, id ObjectID, indexFilename ...string) error { var env []string if len(indexFilename) > 0 { env = append(os.Environ(), "GIT_INDEX_FILE="+indexFilename[0]) } - _, _, err := gitcmd.NewCommand("read-tree").AddDynamicArguments(id.String()).WithDir(repo.Path).WithEnv(env).RunStdString(repo.Ctx) + _, _, err := gitcmd.NewCommand("read-tree").AddDynamicArguments(id.String()).WithDir(repo.Path).WithEnv(env).RunStdString(ctx) if err != nil { return err } @@ -50,7 +50,7 @@ func (repo *Repository) readTreeToIndex(id ObjectID, indexFilename ...string) er } // ReadTreeToTemporaryIndex reads a treeish to a temporary index file -func (repo *Repository) ReadTreeToTemporaryIndex(treeish string) (tmpIndexFilename, tmpDir string, cancel context.CancelFunc, err error) { +func (repo *Repository) ReadTreeToTemporaryIndex(ctx context.Context, treeish string) (tmpIndexFilename, tmpDir string, cancel context.CancelFunc, err error) { defer func() { // if error happens and there is a cancel function, do clean up if err != nil && cancel != nil { @@ -66,7 +66,7 @@ func (repo *Repository) ReadTreeToTemporaryIndex(treeish string) (tmpIndexFilena tmpIndexFilename = filepath.Join(tmpDir, ".tmp-index") - err = repo.ReadTreeToIndex(treeish, tmpIndexFilename) + err = repo.ReadTreeToIndex(ctx, treeish, tmpIndexFilename) if err != nil { return "", "", cancel, err } @@ -74,15 +74,15 @@ func (repo *Repository) ReadTreeToTemporaryIndex(treeish string) (tmpIndexFilena } // EmptyIndex empties the index -func (repo *Repository) EmptyIndex() error { - _, _, err := gitcmd.NewCommand("read-tree", "--empty").WithDir(repo.Path).RunStdString(repo.Ctx) +func (repo *Repository) EmptyIndex(ctx context.Context) error { + _, _, err := gitcmd.NewCommand("read-tree", "--empty").WithDir(repo.Path).RunStdString(ctx) return err } // LsFiles checks if the given filenames are in the index -func (repo *Repository) LsFiles(filenames ...string) ([]string, error) { +func (repo *Repository) LsFiles(ctx context.Context, filenames ...string) ([]string, error) { cmd := gitcmd.NewCommand("ls-files", "-z").AddDashesAndList(filenames...) - res, _, err := cmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) + res, _, err := cmd.WithDir(repo.Path).RunStdBytes(ctx) if err != nil { return nil, err } @@ -95,8 +95,8 @@ func (repo *Repository) LsFiles(filenames ...string) ([]string, error) { } // RemoveFilesFromIndex removes given filenames from the index - it does not check whether they are present. -func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error { - objectFormat, err := repo.GetObjectFormat() +func (repo *Repository) RemoveFilesFromIndex(ctx context.Context, filenames ...string) error { + objectFormat, err := repo.GetObjectFormat(ctx) if err != nil { return err } @@ -111,7 +111,7 @@ func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error { return cmd. WithDir(repo.Path). WithStdinBytes(input.Bytes()). - RunWithStderr(repo.Ctx) + RunWithStderr(ctx) } type IndexObjectInfo struct { @@ -121,7 +121,7 @@ type IndexObjectInfo struct { } // AddObjectsToIndex adds the provided object hashes to the index at the provided filenames -func (repo *Repository) AddObjectsToIndex(objects ...IndexObjectInfo) error { +func (repo *Repository) AddObjectsToIndex(ctx context.Context, objects ...IndexObjectInfo) error { cmd := gitcmd.NewCommand("update-index", "--add", "--replace", "-z", "--index-info") input := new(bytes.Buffer) for _, object := range objects { @@ -131,17 +131,17 @@ func (repo *Repository) AddObjectsToIndex(objects ...IndexObjectInfo) error { return cmd. WithDir(repo.Path). WithStdinBytes(input.Bytes()). - RunWithStderr(repo.Ctx) + RunWithStderr(ctx) } // AddObjectToIndex adds the provided object hash to the index at the provided filename -func (repo *Repository) AddObjectToIndex(mode string, object ObjectID, filename string) error { - return repo.AddObjectsToIndex(IndexObjectInfo{Mode: mode, Object: object, Filename: filename}) +func (repo *Repository) AddObjectToIndex(ctx context.Context, mode string, object ObjectID, filename string) error { + return repo.AddObjectsToIndex(ctx, IndexObjectInfo{Mode: mode, Object: object, Filename: filename}) } // WriteTree writes the current index as a tree to the object db and returns its hash -func (repo *Repository) WriteTree() (*Tree, error) { - stdout, _, runErr := gitcmd.NewCommand("write-tree").WithDir(repo.Path).RunStdString(repo.Ctx) +func (repo *Repository) WriteTree(ctx context.Context) (*Tree, error) { + stdout, _, runErr := gitcmd.NewCommand("write-tree").WithDir(repo.Path).RunStdString(ctx) if runErr != nil { return nil, runErr } @@ -149,5 +149,5 @@ func (repo *Repository) WriteTree() (*Tree, error) { if err != nil { return nil, err } - return NewTree(repo, id), nil + return newTree(id), nil } diff --git a/modules/git/repo_object.go b/modules/git/repo_object.go index 5398973ef6..c139d2a69e 100644 --- a/modules/git/repo_object.go +++ b/modules/git/repo_object.go @@ -5,6 +5,7 @@ package git import ( + "context" "strings" "gitea.dev/modules/git/gitcmd" @@ -31,12 +32,12 @@ func (o ObjectType) Bytes() []byte { return []byte(o) } -func (repo *Repository) GetObjectFormat() (ObjectFormat, error) { - if repo != nil && repo.objectFormat != nil { - return repo.objectFormat, nil +func (repo *Repository) GetObjectFormat(ctx context.Context) (ObjectFormat, error) { + if repo.objectFormatCache != nil { + return repo.objectFormatCache, nil } - str, err := repo.hashObjectBytes(nil, false) + str, err := repo.hashObjectBytes(ctx, nil, false) if err != nil { return nil, err } @@ -45,21 +46,21 @@ func (repo *Repository) GetObjectFormat() (ObjectFormat, error) { return nil, err } - repo.objectFormat = hash.Type() + repo.objectFormatCache = hash.Type() - return repo.objectFormat, nil + return repo.objectFormatCache, nil } // HashObjectBytes returns hash for the content -func (repo *Repository) HashObjectBytes(buf []byte) (ObjectID, error) { - idStr, err := repo.hashObjectBytes(buf, true) +func (repo *Repository) HashObjectBytes(ctx context.Context, buf []byte) (ObjectID, error) { + idStr, err := repo.hashObjectBytes(ctx, buf, true) if err != nil { return nil, err } return NewIDFromString(idStr) } -func (repo *Repository) hashObjectBytes(buf []byte, save bool) (string, error) { +func (repo *Repository) hashObjectBytes(ctx context.Context, buf []byte, save bool) (string, error) { var cmd *gitcmd.Command if save { cmd = gitcmd.NewCommand("hash-object", "-w", "--stdin") @@ -69,7 +70,7 @@ func (repo *Repository) hashObjectBytes(buf []byte, save bool) (string, error) { stdout, _, err := cmd. WithDir(repo.Path). WithStdinBytes(buf). - RunStdString(repo.Ctx) + RunStdString(ctx) if err != nil { return "", err } diff --git a/modules/git/repo_ref.go b/modules/git/repo_ref.go index 11235c71b1..974dcf1829 100644 --- a/modules/git/repo_ref.go +++ b/modules/git/repo_ref.go @@ -13,8 +13,8 @@ import ( ) // GetRefs returns all references of the repository. -func (repo *Repository) GetRefs() ([]*Reference, error) { - return repo.GetRefsFiltered("") +func (repo *Repository) GetRefs(ctx context.Context) ([]*Reference, error) { + return repo.GetRefsFiltered(ctx, "") } // ListOccurrences lists all refs of the given refType the given commit appears in sorted by creation date DESC @@ -72,19 +72,19 @@ func parseTags(refs []string) []string { // * "refs/tags/1234567890" vs commit "1234567890" // In most cases, it SHOULD AVOID using this function, unless there is an irresistible reason (eg: make API friendly to end users) // If the function is used, the caller SHOULD CHECK the ref type carefully. -func (repo *Repository) UnstableGuessRefByShortName(shortName string) RefName { - if repo.IsBranchExist(shortName) { +func (repo *Repository) UnstableGuessRefByShortName(ctx context.Context, shortName string) RefName { + if repo.IsBranchExist(ctx, shortName) { return RefNameFromBranch(shortName) } - if repo.IsTagExist(shortName) { + if repo.IsTagExist(ctx, shortName) { return RefNameFromTag(shortName) } if strings.HasPrefix(shortName, "refs/") { - if repo.IsReferenceExist(shortName) { + if repo.IsReferenceExist(ctx, shortName) { return RefName(shortName) } } - commit, err := repo.GetCommit(shortName) + commit, err := repo.GetCommit(ctx, shortName) if err == nil { commitIDString := commit.ID.String() // make sure the "shortName" is either partial commit ID, or it is HEAD diff --git a/modules/git/repo_ref_gogit.go b/modules/git/repo_ref_gogit.go index fc43ce5545..2c2407e980 100644 --- a/modules/git/repo_ref_gogit.go +++ b/modules/git/repo_ref_gogit.go @@ -6,6 +6,7 @@ package git import ( + "context" "strings" "github.com/go-git/go-git/v5" @@ -13,7 +14,7 @@ import ( ) // GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with. -func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) { +func (repo *Repository) GetRefsFiltered(ctx context.Context, pattern string) ([]*Reference, error) { r, err := git.PlainOpen(repo.Path) if err != nil { return nil, err @@ -30,7 +31,7 @@ func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) { refType := string(ObjectCommit) if ref.Name().IsTag() { // tags can be of type `commit` (lightweight) or `tag` (annotated) - if tagType, _ := repo.GetTagType(ParseGogitHash(ref.Hash())); err == nil { + if tagType, _ := repo.GetTagType(ctx, ParseGogitHash(ref.Hash())); err == nil { refType = tagType } } diff --git a/modules/git/repo_ref_nogogit.go b/modules/git/repo_ref_nogogit.go index 918581e06a..d3fcb05768 100644 --- a/modules/git/repo_ref_nogogit.go +++ b/modules/git/repo_ref_nogogit.go @@ -7,6 +7,7 @@ package git import ( "bufio" + "context" "io" "strings" @@ -14,7 +15,7 @@ import ( ) // GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with. -func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) { +func (repo *Repository) GetRefsFiltered(ctx context.Context, pattern string) ([]*Reference, error) { refs := make([]*Reference, 0) cmd := gitcmd.NewCommand("for-each-ref") stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe() @@ -70,6 +71,6 @@ func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) { } } return nil - }).RunWithStderr(repo.Ctx) + }).RunWithStderr(ctx) return refs, err } diff --git a/modules/git/repo_ref_test.go b/modules/git/repo_ref_test.go index 29c255098f..e3e71328bf 100644 --- a/modules/git/repo_ref_test.go +++ b/modules/git/repo_ref_test.go @@ -12,11 +12,11 @@ import ( func TestRepository_GetRefs(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) + bareRepo1, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer bareRepo1.Close() - refs, err := bareRepo1.GetRefs() + refs, err := bareRepo1.GetRefs(t.Context()) assert.NoError(t, err) assert.Len(t, refs, 6) @@ -37,11 +37,11 @@ func TestRepository_GetRefs(t *testing.T) { func TestRepository_GetRefsFiltered(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) + bareRepo1, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer bareRepo1.Close() - refs, err := bareRepo1.GetRefsFiltered(TagPrefix) + refs, err := bareRepo1.GetRefsFiltered(t.Context(), TagPrefix) assert.NoError(t, err) if assert.Len(t, refs, 2) { diff --git a/modules/git/repo_stats.go b/modules/git/repo_stats.go index 716bf0ba05..8190b454a2 100644 --- a/modules/git/repo_stats.go +++ b/modules/git/repo_stats.go @@ -5,6 +5,7 @@ package git import ( "bufio" + "context" "fmt" "sort" "strconv" @@ -34,7 +35,7 @@ type CodeActivityAuthor struct { } // GetCodeActivityStats returns code statistics for activity page -func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) (*CodeActivityStats, error) { +func (repo *Repository) GetCodeActivityStats(ctx context.Context, fromTime time.Time, branch string) (*CodeActivityStats, error) { stats := &CodeActivityStats{} since := fromTime.Format(time.RFC3339) @@ -42,7 +43,7 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) stdout, _, runErr := gitcmd.NewCommand("rev-list", "--count", "--no-merges", "--branches=*", "--date=iso"). AddOptionFormat("--since=%s", since). WithDir(repo.Path). - RunStdString(repo.Ctx) + RunStdString(ctx) if runErr != nil { return nil, runErr } @@ -131,7 +132,7 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) stats.Authors = a return nil }). - RunWithStderr(repo.Ctx) + RunWithStderr(ctx) if err != nil { return nil, fmt.Errorf("GetCodeActivityStats: %w", err) } diff --git a/modules/git/repo_stats_test.go b/modules/git/repo_stats_test.go index 538283111b..143999e93b 100644 --- a/modules/git/repo_stats_test.go +++ b/modules/git/repo_stats_test.go @@ -13,14 +13,14 @@ import ( func TestRepository_GetCodeActivityStats(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) + bareRepo1, err := OpenRepositoryLocal(bareRepo1Path) assert.NoError(t, err) defer bareRepo1.Close() timeFrom, err := time.Parse(time.RFC3339, "2016-01-01T00:00:00+00:00") assert.NoError(t, err) - code, err := bareRepo1.GetCodeActivityStats(timeFrom, "") + code, err := bareRepo1.GetCodeActivityStats(t.Context(), timeFrom, "") assert.NoError(t, err) assert.NotNil(t, code) diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 0a42401611..ab404ce390 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -5,6 +5,7 @@ package git import ( + "context" "fmt" "strings" @@ -17,28 +18,28 @@ import ( const TagPrefix = "refs/tags/" // CreateTag create one tag in the repository -func (repo *Repository) CreateTag(name, revision string) error { - _, _, err := gitcmd.NewCommand("tag").AddDashesAndList(name, revision).WithDir(repo.Path).RunStdString(repo.Ctx) +func (repo *Repository) CreateTag(ctx context.Context, name, revision string) error { + _, _, err := gitcmd.NewCommand("tag").AddDashesAndList(name, revision).WithDir(repo.Path).RunStdString(ctx) return err } // CreateAnnotatedTag create one annotated tag in the repository -func (repo *Repository) CreateAnnotatedTag(name, message, revision string) error { +func (repo *Repository) CreateAnnotatedTag(ctx context.Context, name, message, revision string) error { _, _, err := gitcmd.NewCommand("tag", "-a", "-m"). AddDynamicArguments(message). AddDashesAndList(name, revision). WithDir(repo.Path). - RunStdString(repo.Ctx) + RunStdString(ctx) return err } // GetTagNameBySHA returns the name of a tag from its tag object SHA or commit SHA -func (repo *Repository) GetTagNameBySHA(sha string) (string, error) { +func (repo *Repository) GetTagNameBySHA(ctx context.Context, sha string) (string, error) { if len(sha) < 5 { return "", fmt.Errorf("SHA is too short: %s", sha) } - stdout, _, err := gitcmd.NewCommand("show-ref", "--tags", "-d").WithDir(repo.Path).RunStdString(repo.Ctx) + stdout, _, err := gitcmd.NewCommand("show-ref", "--tags", "-d").WithDir(repo.Path).RunStdString(ctx) if err != nil { return "", err } @@ -60,8 +61,8 @@ func (repo *Repository) GetTagNameBySHA(sha string) (string, error) { } // GetTagID returns the object ID for a tag (annotated tags have both an object SHA AND a commit SHA) -func (repo *Repository) GetTagID(name string) (string, error) { - stdout, _, err := gitcmd.NewCommand("show-ref", "--tags").AddDashesAndList(name).WithDir(repo.Path).RunStdString(repo.Ctx) +func (repo *Repository) GetTagID(ctx context.Context, name string) (string, error) { + stdout, _, err := gitcmd.NewCommand("show-ref", "--tags").AddDashesAndList(name).WithDir(repo.Path).RunStdString(ctx) if err != nil { return "", err } @@ -76,8 +77,8 @@ func (repo *Repository) GetTagID(name string) (string, error) { } // GetTag returns a Git tag by given name. -func (repo *Repository) GetTag(name string) (*Tag, error) { - idStr, err := repo.GetTagID(name) +func (repo *Repository) GetTag(ctx context.Context, name string) (*Tag, error) { + idStr, err := repo.GetTagID(ctx, name) if err != nil { return nil, err } @@ -87,7 +88,7 @@ func (repo *Repository) GetTag(name string) (*Tag, error) { return nil, err } - tag, err := repo.getTag(id, name) + tag, err := repo.getTag(ctx, id, name) if err != nil { return nil, err } @@ -95,13 +96,13 @@ func (repo *Repository) GetTag(name string) (*Tag, error) { } // GetTagWithID returns a Git tag by given name and ID -func (repo *Repository) GetTagWithID(idStr, name string) (*Tag, error) { +func (repo *Repository) GetTagWithID(ctx context.Context, idStr, name string) (*Tag, error) { id, err := NewIDFromString(idStr) if err != nil { return nil, err } - tag, err := repo.getTag(id, name) + tag, err := repo.getTag(ctx, id, name) if err != nil { return nil, err } @@ -109,7 +110,7 @@ func (repo *Repository) GetTagWithID(idStr, name string) (*Tag, error) { } // GetTagInfos returns all tag infos of the repository. -func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { +func (repo *Repository) GetTagInfos(ctx context.Context, page, pageSize int) ([]*Tag, int, error) { // Generally, refname:short should be equal to refname:lstrip=2 except core.warnAmbiguousRefs is used to select the strict abbreviation mode. // https://git-scm.com/docs/git-for-each-ref#Documentation/git-for-each-ref.txt-refname forEachRefFmt := foreachref.NewFormat("objecttype", "refname:lstrip=2", "object", "objectname", "creator", "contents", "contents:signature") @@ -147,7 +148,7 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { } return nil }). - RunWithStderr(repo.Ctx) + RunWithStderr(ctx) return tags, tagsTotal, err } @@ -194,14 +195,14 @@ func parseTagRef(ref map[string]string) (tag *Tag, err error) { } // GetAnnotatedTag returns a Git tag by its SHA, must be an annotated tag -func (repo *Repository) GetAnnotatedTag(sha string) (*Tag, error) { +func (repo *Repository) GetAnnotatedTag(ctx context.Context, sha string) (*Tag, error) { id, err := NewIDFromString(sha) if err != nil { return nil, err } // Tag type must be "tag" (annotated) and not a "commit" (lightweight) tag - if tagType, err := repo.GetTagType(id); err != nil { + if tagType, err := repo.GetTagType(ctx, id); err != nil { return nil, err } else if ObjectType(tagType) != ObjectTag { // not an annotated tag @@ -209,12 +210,12 @@ func (repo *Repository) GetAnnotatedTag(sha string) (*Tag, error) { } // Get tag name - name, err := repo.GetTagNameBySHA(id.String()) + name, err := repo.GetTagNameBySHA(ctx, id.String()) if err != nil { return nil, err } - tag, err := repo.getTag(id, name) + tag, err := repo.getTag(ctx, id, name) if err != nil { return nil, err } diff --git a/modules/git/repo_tag_gogit.go b/modules/git/repo_tag_gogit.go index bcb5826623..4e256189c7 100644 --- a/modules/git/repo_tag_gogit.go +++ b/modules/git/repo_tag_gogit.go @@ -7,19 +7,21 @@ package git import ( + "context" + "gitea.dev/modules/log" "github.com/go-git/go-git/v5/plumbing" ) // IsTagExist returns true if given tag exists in the repository. -func (repo *Repository) IsTagExist(name string) bool { +func (repo *Repository) IsTagExist(_ context.Context, name string) bool { _, err := repo.gogitRepo.Reference(plumbing.ReferenceName(TagPrefix+name), true) return err == nil } // GetTagType gets the type of the tag, either commit (simple) or tag (annotated) -func (repo *Repository) GetTagType(id ObjectID) (string, error) { +func (repo *Repository) GetTagType(_ context.Context, id ObjectID) (string, error) { // Get tag type obj, err := repo.gogitRepo.Object(plumbing.AnyObject, plumbing.Hash(id.RawValue())) if err != nil { @@ -32,7 +34,7 @@ func (repo *Repository) GetTagType(id ObjectID) (string, error) { return obj.Type().String(), nil } -func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { +func (repo *Repository) getTag(ctx context.Context, tagID ObjectID, name string) (*Tag, error) { t, ok := repo.tagCache.Get(tagID.String()) if ok { log.Debug("Hit cache: %s", tagID) @@ -41,13 +43,13 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { return &tagClone, nil } - tp, err := repo.GetTagType(tagID) + tp, err := repo.GetTagType(ctx, tagID) if err != nil { return nil, err } // Get the commit ID and tag ID (may be different for annotated tag) for the returned tag object - commitIDStr, err := repo.GetTagCommitID(name) + commitIDStr, err := repo.GetTagCommitID(ctx, name) if err != nil { // every tag should have a commit ID so return all errors return nil, err @@ -59,7 +61,7 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { // If type is "commit, the tag is a lightweight tag if ObjectType(tp) == ObjectCommit { - commit, err := repo.GetCommit(commitIDStr) + commit, err := repo.GetCommit(ctx, commitIDStr) if err != nil { return nil, err } diff --git a/modules/git/repo_tag_nogogit.go b/modules/git/repo_tag_nogogit.go index 184e125a8f..ecb5bc8f5f 100644 --- a/modules/git/repo_tag_nogogit.go +++ b/modules/git/repo_tag_nogogit.go @@ -7,6 +7,7 @@ package git import ( + "context" "errors" "io" @@ -14,17 +15,17 @@ import ( ) // IsTagExist returns true if given tag exists in the repository. -func (repo *Repository) IsTagExist(name string) bool { +func (repo *Repository) IsTagExist(ctx context.Context, name string) bool { if repo == nil || name == "" { return false } - return repo.IsReferenceExist(TagPrefix + name) + return repo.IsReferenceExist(ctx, TagPrefix+name) } // GetTagType gets the type of the tag, either commit (simple) or tag (annotated) -func (repo *Repository) GetTagType(id ObjectID) (string, error) { - batch, cancel, err := repo.CatFileBatch(repo.Ctx) +func (repo *Repository) GetTagType(ctx context.Context, id ObjectID) (string, error) { + batch, cancel, err := repo.CatFileBatch(ctx) if err != nil { return "", err } @@ -39,7 +40,7 @@ func (repo *Repository) GetTagType(id ObjectID) (string, error) { return info.Type, nil } -func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { +func (repo *Repository) getTag(ctx context.Context, tagID ObjectID, name string) (*Tag, error) { t, ok := repo.tagCache.Get(tagID.String()) if ok { log.Debug("Hit cache: %s", tagID) @@ -48,13 +49,13 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { return &tagClone, nil } - tp, err := repo.GetTagType(tagID) + tp, err := repo.GetTagType(ctx, tagID) if err != nil { return nil, err } // Get the commit ID and tag ID (may be different for annotated tag) for the returned tag object - commitIDStr, err := repo.GetTagCommitID(name) + commitIDStr, err := repo.GetTagCommitID(ctx, name) if err != nil { // every tag should have a commit ID so return all errors return nil, err @@ -66,7 +67,7 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { // If type is "commit, the tag is a lightweight tag if ObjectType(tp) == ObjectCommit { - commit, err := repo.GetCommit(commitIDStr) + commit, err := repo.GetCommit(ctx, commitIDStr) if err != nil { return nil, err } @@ -84,7 +85,7 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { } // The tag is an annotated tag with a message. - batch, cancel, err := repo.CatFileBatch(repo.Ctx) + batch, cancel, err := repo.CatFileBatch(ctx) if err != nil { return nil, err } diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go index 50c6ab5036..a75dec4b96 100644 --- a/modules/git/repo_tag_test.go +++ b/modules/git/repo_tag_test.go @@ -13,14 +13,14 @@ import ( func TestRepository_GetTagInfos(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) + bareRepo1, err := OpenRepositoryLocal(bareRepo1Path) if err != nil { assert.NoError(t, err) return } defer bareRepo1.Close() - tags, total, err := bareRepo1.GetTagInfos(0, 0) + tags, total, err := bareRepo1.GetTagInfos(t.Context(), 0, 0) if err != nil { assert.NoError(t, err) return @@ -44,7 +44,7 @@ func TestRepository_GetTag(t *testing.T) { return } - bareRepo1, err := OpenRepository(t.Context(), clonedPath) + bareRepo1, err := OpenRepositoryLocal(clonedPath) if err != nil { assert.NoError(t, err) return @@ -56,14 +56,14 @@ func TestRepository_GetTag(t *testing.T) { lTagName := "lightweightTag" // Create the lightweight tag - err = bareRepo1.CreateTag(lTagName, lTagCommitID) + err = bareRepo1.CreateTag(t.Context(), lTagName, lTagCommitID) if err != nil { assert.NoError(t, err, "Unable to create the lightweight tag: %s for ID: %s. Error: %v", lTagName, lTagCommitID, err) return } // and try to get the Tag for lightweight tag - lTag, err := bareRepo1.GetTag(lTagName) + lTag, err := bareRepo1.GetTag(t.Context(), lTagName) require.NoError(t, err) require.NotNil(t, lTag, "nil lTag: %s", lTagName) @@ -78,20 +78,20 @@ func TestRepository_GetTag(t *testing.T) { aTagMessage := "my annotated message \n - test two line" // Create the annotated tag - err = bareRepo1.CreateAnnotatedTag(aTagName, aTagMessage, aTagCommitID) + err = bareRepo1.CreateAnnotatedTag(t.Context(), aTagName, aTagMessage, aTagCommitID) if err != nil { assert.NoError(t, err, "Unable to create the annotated tag: %s for ID: %s. Error: %v", aTagName, aTagCommitID, err) return } // Now try to get the tag for the annotated Tag - aTagID, err := bareRepo1.GetTagID(aTagName) + aTagID, err := bareRepo1.GetTagID(t.Context(), aTagName) if err != nil { assert.NoError(t, err) return } - aTag, err := bareRepo1.GetTag(aTagName) + aTag, err := bareRepo1.GetTag(t.Context(), aTagName) require.NoError(t, err) require.NotNil(t, aTag, "nil aTag: %s", aTagName) @@ -106,20 +106,20 @@ func TestRepository_GetTag(t *testing.T) { rTagCommitID := "8006ff9adbf0cb94da7dad9e537e53817f9fa5c0" rTagName := "release/" + lTagName - err = bareRepo1.CreateTag(rTagName, rTagCommitID) + err = bareRepo1.CreateTag(t.Context(), rTagName, rTagCommitID) if err != nil { assert.NoError(t, err, "Unable to create the tag: %s for ID: %s. Error: %v", rTagName, rTagCommitID, err) return } - rTagID, err := bareRepo1.GetTagID(rTagName) + rTagID, err := bareRepo1.GetTagID(t.Context(), rTagName) if err != nil { assert.NoError(t, err) return } assert.Equal(t, rTagCommitID, rTagID) - oTagID, err := bareRepo1.GetTagID(lTagName) + oTagID, err := bareRepo1.GetTagID(t.Context(), lTagName) if err != nil { assert.NoError(t, err) return @@ -136,7 +136,7 @@ func TestRepository_GetAnnotatedTag(t *testing.T) { return } - bareRepo1, err := OpenRepository(t.Context(), clonedPath) + bareRepo1, err := OpenRepositoryLocal(clonedPath) if err != nil { assert.NoError(t, err) return @@ -145,16 +145,16 @@ func TestRepository_GetAnnotatedTag(t *testing.T) { lTagCommitID := "6fbd69e9823458e6c4a2fc5c0f6bc022b2f2acd1" lTagName := "lightweightTag" - bareRepo1.CreateTag(lTagName, lTagCommitID) + bareRepo1.CreateTag(t.Context(), lTagName, lTagCommitID) aTagCommitID := "8006ff9adbf0cb94da7dad9e537e53817f9fa5c0" aTagName := "annotatedTag" aTagMessage := "my annotated message" - bareRepo1.CreateAnnotatedTag(aTagName, aTagMessage, aTagCommitID) - aTagID, _ := bareRepo1.GetTagID(aTagName) + bareRepo1.CreateAnnotatedTag(t.Context(), aTagName, aTagMessage, aTagCommitID) + aTagID, _ := bareRepo1.GetTagID(t.Context(), aTagName) // Try an annotated tag - tag, err := bareRepo1.GetAnnotatedTag(aTagID) + tag, err := bareRepo1.GetAnnotatedTag(t.Context(), aTagID) if err != nil { assert.NoError(t, err) return @@ -165,18 +165,18 @@ func TestRepository_GetAnnotatedTag(t *testing.T) { assert.Equal(t, "tag", tag.Type) // Annotated tag's Commit ID should fail - tag2, err := bareRepo1.GetAnnotatedTag(aTagCommitID) + tag2, err := bareRepo1.GetAnnotatedTag(t.Context(), aTagCommitID) assert.Error(t, err) assert.True(t, IsErrNotExist(err)) assert.Nil(t, tag2) // Annotated tag's name should fail - tag3, err := bareRepo1.GetAnnotatedTag(aTagName) + tag3, err := bareRepo1.GetAnnotatedTag(t.Context(), aTagName) assert.Errorf(t, err, "Length must be 40: %d", len(aTagName)) assert.Nil(t, tag3) // Lightweight Tag should fail - tag4, err := bareRepo1.GetAnnotatedTag(lTagCommitID) + tag4, err := bareRepo1.GetAnnotatedTag(t.Context(), lTagCommitID) assert.Error(t, err) assert.True(t, IsErrNotExist(err)) assert.Nil(t, tag4) diff --git a/modules/git/repo_test.go b/modules/git/repo_test.go index be0a21a83d..5e53d9f0eb 100644 --- a/modules/git/repo_test.go +++ b/modules/git/repo_test.go @@ -15,10 +15,10 @@ import ( func TestRepoIsEmpty(t *testing.T) { emptyRepo2Path := filepath.Join(testReposDir, "repo2_empty") - repo, err := OpenRepository(t.Context(), emptyRepo2Path) + repo, err := OpenRepositoryLocal(emptyRepo2Path) assert.NoError(t, err) defer repo.Close() - isEmpty, err := repo.IsEmpty() + isEmpty, err := repo.IsEmpty(t.Context()) assert.NoError(t, err) assert.True(t, isEmpty) } diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go index aa750d3e1e..b2c251a01a 100644 --- a/modules/git/repo_tree.go +++ b/modules/git/repo_tree.go @@ -6,6 +6,7 @@ package git import ( "bytes" + "context" "os" "strings" "time" @@ -23,7 +24,7 @@ type CommitTreeOpts struct { } // CommitTree creates a commit from a given tree id for the user with provided message -func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opts CommitTreeOpts) (ObjectID, error) { +func (repo *Repository) CommitTree(ctx context.Context, author, committer *Signature, tree *Tree, opts CommitTreeOpts) (ObjectID, error) { commitTimeStr := time.Now().Format(time.RFC3339) // Because this may call hooks we should pass in the environment @@ -61,7 +62,7 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt stdout, _, err := cmd.WithEnv(env). WithDir(repo.Path). WithStdinBytes(messageBytes.Bytes()). - RunStdString(repo.Ctx) + RunStdString(ctx) if err != nil { return nil, err } diff --git a/modules/git/repo_tree_gogit.go b/modules/git/repo_tree_gogit.go index 622052d64d..848c2fd14b 100644 --- a/modules/git/repo_tree_gogit.go +++ b/modules/git/repo_tree_gogit.go @@ -7,6 +7,7 @@ package git import ( + "context" "errors" "gitea.dev/modules/git/gitcmd" @@ -14,7 +15,7 @@ import ( "github.com/go-git/go-git/v5/plumbing" ) -func (repo *Repository) getTree(id ObjectID) (*Tree, error) { +func (repo *Repository) getTree(_ context.Context, id ObjectID) (*Tree, error) { gogitTree, err := repo.gogitRepo.TreeObject(plumbing.Hash(id.RawValue())) if err != nil { if errors.Is(err, plumbing.ErrObjectNotFound) { @@ -25,14 +26,14 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) { return nil, err } - tree := NewTree(repo, id) + tree := newTree(id) tree.resolvedGogitTreeObject = gogitTree return tree, nil } // GetTree find the tree object in the repository. -func (repo *Repository) GetTree(idStr string) (*Tree, error) { - objectFormat, err := repo.GetObjectFormat() +func (repo *Repository) GetTree(ctx context.Context, idStr string) (*Tree, error) { + objectFormat, err := repo.GetObjectFormat(ctx) if err != nil { return nil, err } @@ -41,7 +42,7 @@ func (repo *Repository) GetTree(idStr string) (*Tree, error) { res, _, err := gitcmd.NewCommand("rev-parse", "--verify"). AddDynamicArguments(idStr). WithDir(repo.Path). - RunStdString(repo.Ctx) + RunStdString(ctx) if err != nil { return nil, err } @@ -53,15 +54,13 @@ func (repo *Repository) GetTree(idStr string) (*Tree, error) { if err != nil { return nil, err } - resolvedID := id commitObject, err := repo.gogitRepo.CommitObject(plumbing.Hash(id.RawValue())) if err == nil { id = ParseGogitHash(commitObject.TreeHash) } - treeObject, err := repo.getTree(id) + treeObject, err := repo.getTree(ctx, id) if err != nil { return nil, err } - treeObject.ResolvedID = resolvedID return treeObject, nil } diff --git a/modules/git/repo_tree_nogogit.go b/modules/git/repo_tree_nogogit.go index 82a61072c9..4d90bccdbe 100644 --- a/modules/git/repo_tree_nogogit.go +++ b/modules/git/repo_tree_nogogit.go @@ -6,11 +6,12 @@ package git import ( + "context" "io" ) -func (repo *Repository) getTree(id ObjectID) (*Tree, error) { - batch, cancel, err := repo.CatFileBatch(repo.Ctx) +func (repo *Repository) getTree(ctx context.Context, id ObjectID) (*Tree, error) { + batch, cancel, err := repo.CatFileBatch(ctx) if err != nil { return nil, err } @@ -23,7 +24,6 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) { switch info.Type { case "tag": - resolvedID := id data, err := io.ReadAll(io.LimitReader(rd, info.Size)) if err != nil { return nil, err @@ -37,22 +37,21 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) { if err != nil { return nil, err } - commit.Tree.ResolvedID = resolvedID - return &commit.Tree, nil + tree := commit.Tree() + return tree, nil case "commit": - commit, err := CommitFromReader(repo, id, io.LimitReader(rd, info.Size)) + commit, err := CommitFromReader(id, io.LimitReader(rd, info.Size)) if err != nil { return nil, err } if _, err := rd.Discard(1); err != nil { return nil, err } - commit.Tree.ResolvedID = commit.ID - return &commit.Tree, nil + tree := commit.Tree() + return tree, nil case "tree": - tree := NewTree(repo, id) - tree.ResolvedID = id - objectFormat, err := repo.GetObjectFormat() + tree := newTree(id) + objectFormat, err := repo.GetObjectFormat(ctx) if err != nil { return nil, err } @@ -73,13 +72,13 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) { } // GetTree find the tree object in the repository. -func (repo *Repository) GetTree(idStr string) (*Tree, error) { - objectFormat, err := repo.GetObjectFormat() +func (repo *Repository) GetTree(ctx context.Context, idStr string) (*Tree, error) { + objectFormat, err := repo.GetObjectFormat(ctx) if err != nil { return nil, err } if len(idStr) != objectFormat.FullLength() { - res, err := repo.GetRefCommitID(idStr) + res, err := repo.GetRefCommitID(ctx, idStr) if err != nil { return nil, err } @@ -92,5 +91,5 @@ func (repo *Repository) GetTree(idStr string) (*Tree, error) { return nil, err } - return repo.getTree(id) + return repo.getTree(ctx, id) } diff --git a/modules/git/tests/repos/language_stats_repo/COMMIT_EDITMSG b/modules/git/tests/repos/language_stats_repo/COMMIT_EDITMSG deleted file mode 100644 index ec4d890919..0000000000 --- a/modules/git/tests/repos/language_stats_repo/COMMIT_EDITMSG +++ /dev/null @@ -1,3 +0,0 @@ -Add some test files for GetLanguageStats - -Signed-off-by: Andrew Thornton diff --git a/modules/git/tests/repos/repo3_notes/COMMIT_EDITMSG b/modules/git/tests/repos/repo3_notes/COMMIT_EDITMSG deleted file mode 100644 index 0cfbf08886..0000000000 --- a/modules/git/tests/repos/repo3_notes/COMMIT_EDITMSG +++ /dev/null @@ -1 +0,0 @@ -2 diff --git a/modules/git/tree.go b/modules/git/tree.go index 854f45c56f..b22e20a5ac 100644 --- a/modules/git/tree.go +++ b/modules/git/tree.go @@ -6,31 +6,22 @@ package git import ( "bytes" + "context" "strings" "gitea.dev/modules/git/gitcmd" ) type TreeCommon struct { - ID ObjectID - ResolvedID ObjectID - - repo *Repository - ptree *Tree // parent tree + ID ObjectID } -// NewTree create a new tree according the repository and tree id -func NewTree(repo *Repository, id ObjectID) *Tree { - return &Tree{ - TreeCommon: TreeCommon{ - ID: id, - repo: repo, - }, - } +func newTree(id ObjectID) *Tree { + return &Tree{TreeCommon: TreeCommon{ID: id}} } // SubTree get a subtree by the sub dir path -func (t *Tree) SubTree(rpath string) (*Tree, error) { +func (t *Tree) SubTree(ctx context.Context, gitRepo *Repository, rpath string) (*Tree, error) { if len(rpath) == 0 { return t, nil } @@ -43,27 +34,26 @@ func (t *Tree) SubTree(rpath string) (*Tree, error) { te *TreeEntry ) for _, name := range paths { - te, err = p.GetTreeEntryByPath(name) + te, err = p.GetTreeEntryByPath(ctx, gitRepo, name) if err != nil { return nil, err } - g, err = t.repo.getTree(te.ID) + g, err = gitRepo.getTree(ctx, te.ID) if err != nil { return nil, err } - g.ptree = p p = g } return g, nil } // LsTree checks if the given filenames are in the tree -func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error) { +func (repo *Repository) LsTree(ctx context.Context, ref string, filenames ...string) ([]string, error) { cmd := gitcmd.NewCommand("ls-tree", "-z", "--name-only"). AddDashesAndList(append([]string{ref}, filenames...)...) - res, _, err := cmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) + res, _, err := cmd.WithDir(repo.Path).RunStdBytes(ctx) if err != nil { return nil, err } @@ -76,13 +66,13 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error } // GetTreePathLatestCommit returns the latest commit of a tree path -func (repo *Repository) GetTreePathLatestCommit(refName, treePath string) (*Commit, error) { +func (repo *Repository) GetTreePathLatestCommit(ctx context.Context, refName, treePath string) (*Commit, error) { stdout, _, err := gitcmd.NewCommand("rev-list", "-1"). AddDynamicArguments(refName).AddDashesAndList(treePath). WithDir(repo.Path). - RunStdString(repo.Ctx) + RunStdString(ctx) if err != nil { return nil, err } - return repo.GetCommit(strings.TrimSpace(stdout)) + return repo.GetCommit(ctx, strings.TrimSpace(stdout)) } diff --git a/modules/git/tree_blob.go b/modules/git/tree_blob.go index 31d9f3d73b..8f36bce35b 100644 --- a/modules/git/tree_blob.go +++ b/modules/git/tree_blob.go @@ -4,15 +4,17 @@ package git +import "context" + // GetBlobByPath get the blob object according the path -func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) { - entry, err := t.GetTreeEntryByPath(relpath) +func (t *Tree) GetBlobByPath(ctx context.Context, gitRepo *Repository, relpath string) (*Blob, error) { + entry, err := t.GetTreeEntryByPath(ctx, gitRepo, relpath) if err != nil { return nil, err } if !entry.IsDir() && !entry.IsSubModule() { - return entry.Blob(), nil + return entry.Blob(gitRepo), nil } return nil, ErrNotExist{"", relpath} diff --git a/modules/git/tree_blob_gogit.go b/modules/git/tree_blob_gogit.go index 2c0ff0e1b0..84f08c2e66 100644 --- a/modules/git/tree_blob_gogit.go +++ b/modules/git/tree_blob_gogit.go @@ -7,6 +7,7 @@ package git import ( + "context" "path" "strings" @@ -14,7 +15,7 @@ import ( ) // GetTreeEntryByPath get the tree entries according the sub dir -func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { +func (t *Tree) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (*TreeEntry, error) { if len(relpath) == 0 { return &TreeEntry{ ID: t.ID, @@ -30,7 +31,7 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { tree := t for i, name := range parts { if i == len(parts)-1 { - entries, err := tree.ListEntries() + entries, err := tree.ListEntries(ctx, gitRepo) if err != nil { if err == plumbing.ErrObjectNotFound { return nil, ErrNotExist{ @@ -45,7 +46,7 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { } } } else { - tree, err = tree.SubTree(name) + tree, err = tree.SubTree(ctx, gitRepo, name) if err != nil { if err == plumbing.ErrObjectNotFound { return nil, ErrNotExist{ diff --git a/modules/git/tree_blob_nogogit.go b/modules/git/tree_blob_nogogit.go index b18d0fa05e..2683072818 100644 --- a/modules/git/tree_blob_nogogit.go +++ b/modules/git/tree_blob_nogogit.go @@ -6,12 +6,13 @@ package git import ( + "context" "path" "strings" ) // GetTreeEntryByPath get the tree entries according the sub dir -func (t *Tree) GetTreeEntryByPath(relpath string) (_ *TreeEntry, err error) { +func (t *Tree) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (_ *TreeEntry, err error) { if len(relpath) == 0 { return &TreeEntry{ ptree: t, @@ -26,14 +27,14 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (_ *TreeEntry, err error) { tree := t for _, name := range parts[:len(parts)-1] { - tree, err = tree.SubTree(name) + tree, err = tree.SubTree(ctx, gitRepo, name) if err != nil { return nil, err } } name := parts[len(parts)-1] - entries, err := tree.ListEntries() + entries, err := tree.ListEntries(ctx, gitRepo) if err != nil { return nil, err } diff --git a/modules/git/tree_entry.go b/modules/git/tree_entry.go index bd7e5131b5..5d1c0e3d77 100644 --- a/modules/git/tree_entry.go +++ b/modules/git/tree_entry.go @@ -5,6 +5,7 @@ package git import ( + "context" "path" "slices" "strings" @@ -78,18 +79,18 @@ type EntryFollowResult struct { TargetEntry *TreeEntry } -func EntryFollowLink(commit *Commit, fullPath string, te *TreeEntry) (*EntryFollowResult, error) { +func EntryFollowLink(ctx context.Context, gitRepo *Repository, commit *Commit, fullPath string, te *TreeEntry) (*EntryFollowResult, error) { if !te.IsLink() { return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q is not a symlink", fullPath) } // git's filename max length is 4096, hopefully a link won't be longer than multiple of that const maxSymlinkSize = 20 * 4096 - if te.Blob().Size() > maxSymlinkSize { + if te.Blob(gitRepo).Size(ctx) > maxSymlinkSize { return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q content exceeds symlink limit", fullPath) } - link, err := te.Blob().GetBlobContent(maxSymlinkSize) + link, err := te.Blob(gitRepo).GetBlobContent(ctx, maxSymlinkSize) if err != nil { return nil, err } @@ -99,18 +100,18 @@ func EntryFollowLink(commit *Commit, fullPath string, te *TreeEntry) (*EntryFoll } targetFullPath := path.Join(path.Dir(fullPath), link) - targetEntry, err := commit.GetTreeEntryByPath(targetFullPath) + targetEntry, err := commit.GetTreeEntryByPath(ctx, gitRepo, targetFullPath) if err != nil { return &EntryFollowResult{SymlinkContent: link}, err } return &EntryFollowResult{SymlinkContent: link, TargetFullPath: targetFullPath, TargetEntry: targetEntry}, nil } -func EntryFollowLinks(commit *Commit, firstFullPath string, firstTreeEntry *TreeEntry, optLimit ...int) (res *EntryFollowResult, err error) { +func EntryFollowLinks(ctx context.Context, gitRepo *Repository, commit *Commit, firstFullPath string, firstTreeEntry *TreeEntry, optLimit ...int) (res *EntryFollowResult, err error) { limit := util.OptionalArg(optLimit, 10) treeEntry, fullPath := firstTreeEntry, firstFullPath for range limit { - res, err = EntryFollowLink(commit, fullPath, treeEntry) + res, err = EntryFollowLink(ctx, gitRepo, commit, fullPath, treeEntry) if err != nil { return res, err } @@ -125,28 +126,26 @@ func EntryFollowLinks(commit *Commit, firstFullPath string, firstTreeEntry *Tree return res, nil } -// returns the Tree pointed to by this TreeEntry, or nil if this is not a tree -func (te *TreeEntry) Tree() *Tree { - t, err := te.ptree.repo.getTree(te.ID) +func (te *TreeEntry) Tree(ctx context.Context, gitRepo *Repository) *Tree { + t, err := gitRepo.getTree(ctx, te.ID) if err != nil { return nil } - t.ptree = te.ptree return t } // GetSubJumpablePathName return the full path of subdirectory jumpable ( contains only one directory ) -func (te *TreeEntry) GetSubJumpablePathName() string { +func (te *TreeEntry) GetSubJumpablePathName(ctx context.Context, gitRepo *Repository) string { if te.IsSubModule() || !te.IsDir() { return "" } - tree, err := te.ptree.SubTree(te.Name()) + tree, err := te.ptree.SubTree(ctx, gitRepo, te.Name()) if err != nil { return te.Name() } - entries, _ := tree.ListEntries() + entries, _ := tree.ListEntries(ctx, gitRepo) if len(entries) == 1 && entries[0].IsDir() { - name := entries[0].GetSubJumpablePathName() + name := entries[0].GetSubJumpablePathName(ctx, gitRepo) if name != "" { return te.Name() + "/" + name } diff --git a/modules/git/tree_entry_common_test.go b/modules/git/tree_entry_common_test.go index 2907077693..dfc785f710 100644 --- a/modules/git/tree_entry_common_test.go +++ b/modules/git/tree_entry_common_test.go @@ -4,6 +4,7 @@ package git import ( + "path/filepath" "testing" "gitea.dev/modules/util" @@ -13,22 +14,22 @@ import ( ) func TestFollowLink(t *testing.T) { - r, err := OpenRepository(t.Context(), "tests/repos/repo1_bare") + r, err := OpenRepositoryLocal(filepath.Join(testReposDir, "repo1_bare")) require.NoError(t, err) defer r.Close() - commit, err := r.GetCommit("37991dec2c8e592043f47155ce4808d4580f9123") + commit, err := r.GetCommit(t.Context(), "37991dec2c8e592043f47155ce4808d4580f9123") require.NoError(t, err) // get the symlink { lnkFullPath := "foo/bar/link_to_hello" - lnk, err := commit.Tree.GetTreeEntryByPath("foo/bar/link_to_hello") + lnk, err := commit.Tree().GetTreeEntryByPath(t.Context(), r, "foo/bar/link_to_hello") require.NoError(t, err) assert.True(t, lnk.IsLink()) // should be able to dereference to target - res, err := EntryFollowLink(commit, lnkFullPath, lnk) + res, err := EntryFollowLink(t.Context(), r, commit, lnkFullPath, lnk) require.NoError(t, err) assert.Equal(t, "hello", res.TargetEntry.Name()) assert.Equal(t, "foo/nar/hello", res.TargetFullPath) @@ -38,38 +39,38 @@ func TestFollowLink(t *testing.T) { { // should error when called on a normal file - entry, err := commit.Tree.GetTreeEntryByPath("file1.txt") + entry, err := commit.Tree().GetTreeEntryByPath(t.Context(), r, "file1.txt") require.NoError(t, err) - res, err := EntryFollowLink(commit, "file1.txt", entry) + res, err := EntryFollowLink(t.Context(), r, commit, "file1.txt", entry) assert.ErrorIs(t, err, util.ErrUnprocessableContent) assert.Nil(t, res) } { // should error for broken links - entry, err := commit.Tree.GetTreeEntryByPath("foo/broken_link") + entry, err := commit.Tree().GetTreeEntryByPath(t.Context(), r, "foo/broken_link") require.NoError(t, err) assert.True(t, entry.IsLink()) - res, err := EntryFollowLink(commit, "foo/broken_link", entry) + res, err := EntryFollowLink(t.Context(), r, commit, "foo/broken_link", entry) assert.ErrorIs(t, err, util.ErrNotExist) assert.Equal(t, "nar/broken_link", res.SymlinkContent) } { // should error for external links - entry, err := commit.Tree.GetTreeEntryByPath("foo/outside_repo") + entry, err := commit.Tree().GetTreeEntryByPath(t.Context(), r, "foo/outside_repo") require.NoError(t, err) assert.True(t, entry.IsLink()) - res, err := EntryFollowLink(commit, "foo/outside_repo", entry) + res, err := EntryFollowLink(t.Context(), r, commit, "foo/outside_repo", entry) assert.ErrorIs(t, err, util.ErrNotExist) assert.Equal(t, "../../outside_repo", res.SymlinkContent) } { // testing fix for short link bug - entry, err := commit.Tree.GetTreeEntryByPath("foo/link_short") + entry, err := commit.Tree().GetTreeEntryByPath(t.Context(), r, "foo/link_short") require.NoError(t, err) - res, err := EntryFollowLink(commit, "foo/link_short", entry) + res, err := EntryFollowLink(t.Context(), r, commit, "foo/link_short", entry) assert.ErrorIs(t, err, util.ErrNotExist) assert.Equal(t, "a", res.SymlinkContent) } diff --git a/modules/git/tree_entry_gogit.go b/modules/git/tree_entry_gogit.go index f092e70636..f19d2dc164 100644 --- a/modules/git/tree_entry_gogit.go +++ b/modules/git/tree_entry_gogit.go @@ -7,6 +7,8 @@ package git import ( + "context" + "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/object" @@ -29,15 +31,15 @@ func (te *TreeEntry) toGogitTreeEntry() *object.TreeEntry { } } -// Size returns the size of the entry -func (te *TreeEntry) Size() int64 { +// GetSize returns the size of the entry +func (te *TreeEntry) GetSize(ctx context.Context, gitRepo *Repository) int64 { if te.IsDir() { return 0 } else if te.sized { return te.size } - ptreeGogitTree, err := te.ptree.gogitTreeObject() + ptreeGogitTree, err := te.ptree.gogitTreeObject(gitRepo) if err != nil { return 0 } @@ -52,10 +54,10 @@ func (te *TreeEntry) Size() int64 { } // Blob returns the blob object the entry -func (te *TreeEntry) Blob() *Blob { +func (te *TreeEntry) Blob(gitRepo *Repository) *Blob { return &Blob{ ID: te.ID, - repo: te.ptree.repo, + repo: gitRepo, name: te.Name(), } } diff --git a/modules/git/tree_entry_nogogit.go b/modules/git/tree_entry_nogogit.go index 467a0d7695..53e1e1aef5 100644 --- a/modules/git/tree_entry_nogogit.go +++ b/modules/git/tree_entry_nogogit.go @@ -5,25 +5,28 @@ package git -import "gitea.dev/modules/log" +import ( + "context" -// Size returns the size of the entry -func (te *TreeEntry) Size() int64 { + "gitea.dev/modules/log" +) + +func (te *TreeEntry) GetSize(ctx context.Context, gitRepo *Repository) int64 { if te.IsDir() { return 0 } else if te.sized { return te.size } - batch, cancel, err := te.ptree.repo.CatFileBatch(te.ptree.repo.Ctx) + batch, cancel, err := gitRepo.CatFileBatch(ctx) if err != nil { - log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), te.ptree.repo.Path, err) + log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), gitRepo.Path, err) return 0 } defer cancel() info, err := batch.QueryInfo(te.ID.String()) if err != nil { - log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), te.ptree.repo.Path, err) + log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), gitRepo.Path, err) return 0 } @@ -33,12 +36,12 @@ func (te *TreeEntry) Size() int64 { } // Blob returns the blob object the entry -func (te *TreeEntry) Blob() *Blob { +func (te *TreeEntry) Blob(gitRepo *Repository) *Blob { return &Blob{ ID: te.ID, name: te.Name(), size: te.size, gotSize: te.sized, - repo: te.ptree.repo, + repo: gitRepo, } } diff --git a/modules/git/tree_gogit.go b/modules/git/tree_gogit.go index fec6e2704e..2255b10f59 100644 --- a/modules/git/tree_gogit.go +++ b/modules/git/tree_gogit.go @@ -7,6 +7,7 @@ package git import ( + "context" "io" "github.com/go-git/go-git/v5/plumbing" @@ -20,9 +21,9 @@ type Tree struct { resolvedGogitTreeObject *object.Tree } -func (t *Tree) gogitTreeObject() (_ *object.Tree, err error) { +func (t *Tree) gogitTreeObject(gitRepo *Repository) (_ *object.Tree, err error) { if t.resolvedGogitTreeObject == nil { - t.resolvedGogitTreeObject, err = t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue())) + t.resolvedGogitTreeObject, err = gitRepo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue())) if err != nil { return nil, err } @@ -31,8 +32,8 @@ func (t *Tree) gogitTreeObject() (_ *object.Tree, err error) { } // ListEntries returns all entries of current tree. -func (t *Tree) ListEntries() (Entries, error) { - gogitTree, err := t.gogitTreeObject() +func (t *Tree) ListEntries(_ context.Context, gitRepo *Repository) (Entries, error) { + gogitTree, err := t.gogitTreeObject(gitRepo) if err != nil { return nil, err } @@ -50,8 +51,8 @@ func (t *Tree) ListEntries() (Entries, error) { } // ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees -func (t *Tree) ListEntriesRecursiveWithSize() (entries Entries, _ error) { - gogitTree, err := t.gogitTreeObject() +func (t *Tree) ListEntriesRecursiveWithSize(_ context.Context, gitRepo *Repository) (entries Entries, _ error) { + gogitTree, err := t.gogitTreeObject(gitRepo) if err != nil { return nil, err } @@ -76,6 +77,6 @@ func (t *Tree) ListEntriesRecursiveWithSize() (entries Entries, _ error) { } // ListEntriesRecursiveFast is the alias of ListEntriesRecursiveWithSize for the gogit version -func (t *Tree) ListEntriesRecursiveFast() (Entries, error) { - return t.ListEntriesRecursiveWithSize() +func (t *Tree) ListEntriesRecursiveFast(ctx context.Context, gitRepo *Repository) (Entries, error) { + return t.ListEntriesRecursiveWithSize(ctx, gitRepo) } diff --git a/modules/git/tree_nogogit.go b/modules/git/tree_nogogit.go index ebb4c7bb12..502a3c6585 100644 --- a/modules/git/tree_nogogit.go +++ b/modules/git/tree_nogogit.go @@ -6,6 +6,7 @@ package git import ( + "context" "io" "gitea.dev/modules/git/gitcmd" @@ -20,49 +21,47 @@ type Tree struct { } // ListEntries returns all entries of current tree. -func (t *Tree) ListEntries() (Entries, error) { +func (t *Tree) ListEntries(ctx context.Context, gitRepo *Repository) (Entries, error) { if t.entriesParsed { return t.entries, nil } - if t.repo != nil { - batch, cancel, err := t.repo.CatFileBatch(t.repo.Ctx) - if err != nil { + batch, cancel, err := gitRepo.CatFileBatch(ctx) + if err != nil { + return nil, err + } + defer cancel() + + info, rd, err := batch.QueryContent(t.ID.String()) + if err != nil { + return nil, err + } + + if info.Type == "commit" { + treeID, err := ReadTreeID(rd, info.Size) + if err != nil && err != io.EOF { return nil, err } - defer cancel() - - info, rd, err := batch.QueryContent(t.ID.String()) + info, rd, err = batch.QueryContent(treeID) if err != nil { return nil, err } - - if info.Type == "commit" { - treeID, err := ReadTreeID(rd, info.Size) - if err != nil && err != io.EOF { - return nil, err - } - info, rd, err = batch.QueryContent(treeID) - if err != nil { - return nil, err - } - } - if info.Type == "tree" { - t.entries, err = catBatchParseTreeEntries(t.ID.Type(), t, rd, info.Size) - if err != nil { - return nil, err - } - t.entriesParsed = true - return t.entries, nil - } - - // Not a tree just use ls-tree instead - if err := DiscardFull(rd, info.Size+1); err != nil { + } + if info.Type == "tree" { + t.entries, err = catBatchParseTreeEntries(t.ID.Type(), t, rd, info.Size) + if err != nil { return nil, err } + t.entriesParsed = true + return t.entries, nil } - stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithDir(t.repo.Path).RunStdBytes(t.repo.Ctx) + // Not a tree just use ls-tree instead + if err := DiscardFull(rd, info.Size+1); err != nil { + return nil, err + } + + stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithDir(gitRepo.Path).RunStdBytes(ctx) if runErr != nil { if gitcmd.IsStderr(runErr, gitcmd.StderrNotValidObjectName) || gitcmd.IsStderr(runErr, gitcmd.StderrNotTreeObject) { return nil, ErrNotExist{ @@ -72,7 +71,6 @@ func (t *Tree) ListEntries() (Entries, error) { return nil, runErr } - var err error t.entries, err = parseTreeEntries(stdout, t) if err == nil { t.entriesParsed = true @@ -83,12 +81,12 @@ func (t *Tree) ListEntries() (Entries, error) { // listEntriesRecursive returns all entries of current tree recursively including all subtrees // extraArgs could be "-l" to get the size, which is slower -func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, error) { +func (t *Tree) listEntriesRecursive(ctx context.Context, gitRepo *Repository, extraArgs gitcmd.TrustedCmdArgs) (Entries, error) { stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r"). AddArguments(extraArgs...). AddDynamicArguments(t.ID.String()). - WithDir(t.repo.Path). - RunStdBytes(t.repo.Ctx) + WithDir(gitRepo.Path). + RunStdBytes(ctx) if runErr != nil { return nil, runErr } @@ -99,11 +97,11 @@ func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, e } // ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size -func (t *Tree) ListEntriesRecursiveFast() (Entries, error) { - return t.listEntriesRecursive(nil) +func (t *Tree) ListEntriesRecursiveFast(ctx context.Context, gitRepo *Repository) (Entries, error) { + return t.listEntriesRecursive(ctx, gitRepo, nil) } // ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees, with size -func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) { - return t.listEntriesRecursive(gitcmd.TrustedCmdArgs{"--long"}) +func (t *Tree) ListEntriesRecursiveWithSize(ctx context.Context, gitRepo *Repository) (Entries, error) { + return t.listEntriesRecursive(ctx, gitRepo, gitcmd.TrustedCmdArgs{"--long"}) } diff --git a/modules/git/tree_test.go b/modules/git/tree_test.go index 67f95fe748..06999286cf 100644 --- a/modules/git/tree_test.go +++ b/modules/git/tree_test.go @@ -11,31 +11,31 @@ import ( ) func TestSubTree_Issue29101(t *testing.T) { - repo, err := OpenRepository(t.Context(), filepath.Join(testReposDir, "repo1_bare")) + repo, err := OpenRepositoryLocal(filepath.Join(testReposDir, "repo1_bare")) assert.NoError(t, err) defer repo.Close() - commit, err := repo.GetCommit("ce064814f4a0d337b333e646ece456cd39fab612") + commit, err := repo.GetCommit(t.Context(), "ce064814f4a0d337b333e646ece456cd39fab612") assert.NoError(t, err) // old code could produce a different error if called multiple times for range 10 { - _, err = commit.SubTree("file1.txt") + _, err = commit.SubTree(t.Context(), repo, "file1.txt") assert.Error(t, err) assert.True(t, IsErrNotExist(err)) } } func Test_GetTreePathLatestCommit(t *testing.T) { - repo, err := OpenRepository(t.Context(), filepath.Join(testReposDir, "repo6_blame")) + repo, err := OpenRepositoryLocal(filepath.Join(testReposDir, "repo6_blame")) assert.NoError(t, err) defer repo.Close() - commitID, err := repo.GetBranchCommitID("master") + commitID, err := repo.GetBranchCommitID(t.Context(), "master") assert.NoError(t, err) assert.Equal(t, "544d8f7a3b15927cddf2299b4b562d6ebd71b6a7", commitID) - commit, err := repo.GetTreePathLatestCommit("master", "blame.txt") + commit, err := repo.GetTreePathLatestCommit(t.Context(), "master", "blame.txt") assert.NoError(t, err) assert.NotNil(t, commit) assert.Equal(t, "45fb6cbc12f970b04eacd5cd4165edd11c8d7376", commit.ID.String()) diff --git a/modules/gitrepo/archive.go b/modules/gitrepo/archive.go index d7075d6dd7..b28edfd2ae 100644 --- a/modules/gitrepo/archive.go +++ b/modules/gitrepo/archive.go @@ -10,7 +10,6 @@ import ( "os" "path" "path/filepath" - "slices" "strings" "gitea.dev/modules/git/gitcmd" @@ -18,24 +17,22 @@ import ( ) // CreateArchive create archive content to the target path -func CreateArchive(ctx context.Context, repo Repository, format string, target io.Writer, usePrefix bool, commitID string, paths []string) error { +func CreateArchive(ctx context.Context, repo Repository, repoName, format string, target io.Writer, commitID string, paths []string) error { if format == "unknown" { return fmt.Errorf("unknown format: %v", format) } cmd := gitcmd.NewCommand("archive") - if usePrefix { - cmd.AddOptionFormat("--prefix=%s", filepath.Base(strings.TrimSuffix(repo.RelativePath(), ".git"))+"/") + if setting.Repository.PrefixArchiveFiles { + cmd.AddOptionFormat("--prefix=%s", strings.ToLower(repoName)+"/") } cmd.AddOptionFormat("--format=%s", format) cmd.AddDynamicArguments(commitID) - paths = slices.Clone(paths) for i := range paths { // although "git archive" already ensures the paths won't go outside the repo, we still clean them here for safety - paths[i] = path.Clean(paths[i]) + cmd.AddDynamicArguments(path.Clean(paths[i])) } - cmd.AddDynamicArguments(paths...) return RunCmdWithStderr(ctx, repo, cmd.WithStdoutCopy(target)) } diff --git a/modules/gitrepo/blame.go b/modules/gitrepo/blame.go index e2a5eefa5f..042bbef715 100644 --- a/modules/gitrepo/blame.go +++ b/modules/gitrepo/blame.go @@ -139,7 +139,7 @@ func (r *BlameReader) cleanup() { } // CreateBlameReader creates reader for given repository, commit and file -func CreateBlameReader(ctx context.Context, objectFormat git.ObjectFormat, repo Repository, commit *git.Commit, file string, bypassBlameIgnore bool) (rd *BlameReader, retErr error) { +func CreateBlameReader(ctx context.Context, objectFormat git.ObjectFormat, repo Repository, gitRepo *git.Repository, commit *git.Commit, file string, bypassBlameIgnore bool) (rd *BlameReader, retErr error) { defer func() { if retErr != nil { rd.cleanup() @@ -158,7 +158,7 @@ func CreateBlameReader(ctx context.Context, objectFormat git.ObjectFormat, repo rd.cleanupFuncs = append(rd.cleanupFuncs, stdoutReaderClose) if git.DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore { - ignoreRevsFileName, ignoreRevsFileCleanup, err := tryCreateBlameIgnoreRevsFile(commit) + ignoreRevsFileName, ignoreRevsFileCleanup, err := tryCreateBlameIgnoreRevsFile(ctx, gitRepo, commit) if err != nil && !git.IsErrNotExist(err) { return nil, err } else if err == nil { @@ -180,13 +180,13 @@ func CreateBlameReader(ctx context.Context, objectFormat git.ObjectFormat, repo return rd, nil } -func tryCreateBlameIgnoreRevsFile(commit *git.Commit) (string, func(), error) { - entry, err := commit.GetTreeEntryByPath(".git-blame-ignore-revs") +func tryCreateBlameIgnoreRevsFile(ctx context.Context, gitRepo *git.Repository, commit *git.Commit) (string, func(), error) { + entry, err := commit.GetTreeEntryByPath(ctx, gitRepo, ".git-blame-ignore-revs") if err != nil { return "", nil, err } - r, err := entry.Blob().DataAsync() + r, err := entry.Blob(gitRepo).DataAsync(ctx) if err != nil { return "", nil, err } diff --git a/modules/gitrepo/blame_sha256_test.go b/modules/gitrepo/blame_sha256_test.go index 5577432caf..274cded929 100644 --- a/modules/gitrepo/blame_sha256_test.go +++ b/modules/gitrepo/blame_sha256_test.go @@ -24,12 +24,12 @@ func TestReadingBlameOutputSha256(t *testing.T) { } t.Run("Without .git-blame-ignore-revs", func(t *testing.T) { - storage := &mockRepository{path: "repo5_pulls_sha256"} - repo, err := OpenRepository(ctx, storage) + storage := mockRepository("repo5_pulls_sha256") + repo, err := OpenRepository(storage) assert.NoError(t, err) defer repo.Close() - commit, err := repo.GetCommit("0b69b7bb649b5d46e14cabb6468685e5dd721290acc7ffe604d37cde57927345") + commit, err := repo.GetCommit(t.Context(), "0b69b7bb649b5d46e14cabb6468685e5dd721290acc7ffe604d37cde57927345") assert.NoError(t, err) parts := []*BlamePart{ @@ -49,7 +49,7 @@ func TestReadingBlameOutputSha256(t *testing.T) { } for _, bypass := range []bool{false, true} { - blameReader, err := CreateBlameReader(ctx, git.Sha256ObjectFormat, storage, commit, "README.md", bypass) + blameReader, err := CreateBlameReader(ctx, git.Sha256ObjectFormat, storage, repo, commit, "README.md", bypass) assert.NoError(t, err) assert.NotNil(t, blameReader) defer blameReader.Close() @@ -70,8 +70,8 @@ func TestReadingBlameOutputSha256(t *testing.T) { }) t.Run("With .git-blame-ignore-revs", func(t *testing.T) { - storage := &mockRepository{path: "repo6_blame_sha256"} - repo, err := OpenRepository(ctx, storage) + storage := mockRepository("repo6_blame_sha256") + repo, err := OpenRepository(storage) assert.NoError(t, err) defer repo.Close() @@ -129,12 +129,12 @@ func TestReadingBlameOutputSha256(t *testing.T) { }, } - objectFormat, err := repo.GetObjectFormat() + objectFormat, err := repo.GetObjectFormat(t.Context()) assert.NoError(t, err) for _, c := range cases { - commit, err := repo.GetCommit(c.CommitID) + commit, err := repo.GetCommit(t.Context(), c.CommitID) assert.NoError(t, err) - blameReader, err := CreateBlameReader(ctx, objectFormat, storage, commit, "blame.txt", c.Bypass) + blameReader, err := CreateBlameReader(ctx, objectFormat, storage, repo, commit, "blame.txt", c.Bypass) assert.NoError(t, err) assert.NotNil(t, blameReader) defer blameReader.Close() diff --git a/modules/gitrepo/blame_test.go b/modules/gitrepo/blame_test.go index 54be64f996..6b65851244 100644 --- a/modules/gitrepo/blame_test.go +++ b/modules/gitrepo/blame_test.go @@ -19,11 +19,11 @@ func TestReadingBlameOutput(t *testing.T) { defer cancel() t.Run("Without .git-blame-ignore-revs", func(t *testing.T) { - storage := &mockRepository{path: "repo5_pulls"} - repo, err := OpenRepository(ctx, storage) + storage := mockRepository("repo5_pulls") + repo, err := OpenRepository(storage) assert.NoError(t, err) defer repo.Close() - commit, err := repo.GetCommit("f32b0a9dfd09a60f616f29158f772cedd89942d2") + commit, err := repo.GetCommit(t.Context(), "f32b0a9dfd09a60f616f29158f772cedd89942d2") assert.NoError(t, err) parts := []*BlamePart{ @@ -43,7 +43,7 @@ func TestReadingBlameOutput(t *testing.T) { } for _, bypass := range []bool{false, true} { - blameReader, err := CreateBlameReader(ctx, git.Sha1ObjectFormat, storage, commit, "README.md", bypass) + blameReader, err := CreateBlameReader(ctx, git.Sha1ObjectFormat, storage, repo, commit, "README.md", bypass) assert.NoError(t, err) assert.NotNil(t, blameReader) defer blameReader.Close() @@ -64,8 +64,8 @@ func TestReadingBlameOutput(t *testing.T) { }) t.Run("With .git-blame-ignore-revs", func(t *testing.T) { - storage := &mockRepository{path: "repo6_blame"} - repo, err := OpenRepository(ctx, storage) + storage := mockRepository("repo6_blame") + repo, err := OpenRepository(storage) assert.NoError(t, err) defer repo.Close() @@ -123,13 +123,13 @@ func TestReadingBlameOutput(t *testing.T) { }, } - objectFormat, err := repo.GetObjectFormat() + objectFormat, err := repo.GetObjectFormat(t.Context()) assert.NoError(t, err) for _, c := range cases { - commit, err := repo.GetCommit(c.CommitID) + commit, err := repo.GetCommit(t.Context(), c.CommitID) assert.NoError(t, err) - blameReader, err := CreateBlameReader(ctx, objectFormat, storage, commit, "blame.txt", c.Bypass) + blameReader, err := CreateBlameReader(ctx, objectFormat, storage, repo, commit, "blame.txt", c.Bypass) assert.NoError(t, err) assert.NotNil(t, blameReader) defer blameReader.Close() diff --git a/modules/gitrepo/branch.go b/modules/gitrepo/branch.go index 09594ca35a..9a9d4c8bb5 100644 --- a/modules/gitrepo/branch.go +++ b/modules/gitrepo/branch.go @@ -15,23 +15,23 @@ import ( // GetBranchesByPath returns a branch by its path // if limit = 0 it will not limit func GetBranchesByPath(ctx context.Context, repo Repository, skip, limit int) ([]string, int, error) { - gitRepo, err := OpenRepository(ctx, repo) + gitRepo, err := OpenRepository(repo) if err != nil { return nil, 0, err } defer gitRepo.Close() - return gitRepo.GetBranchNames(skip, limit) + return gitRepo.GetBranchNames(ctx, skip, limit) } func GetBranchCommitID(ctx context.Context, repo Repository, branch string) (string, error) { - gitRepo, err := OpenRepository(ctx, repo) + gitRepo, err := OpenRepository(repo) if err != nil { return "", err } defer gitRepo.Close() - return gitRepo.GetBranchCommitID(branch) + return gitRepo.GetBranchCommitID(ctx, branch) } // SetDefaultBranch sets default branch of repository. diff --git a/modules/gitrepo/cat_file.go b/modules/gitrepo/cat_file.go deleted file mode 100644 index e824c07923..0000000000 --- a/modules/gitrepo/cat_file.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package gitrepo - -import ( - "context" - - "gitea.dev/modules/git" -) - -func NewBatch(ctx context.Context, repo Repository) (git.CatFileBatchCloser, error) { - return git.NewBatch(ctx, repoPath(repo)) -} diff --git a/modules/gitrepo/command.go b/modules/gitrepo/command.go index f672ca1dbd..6572f44f51 100644 --- a/modules/gitrepo/command.go +++ b/modules/gitrepo/command.go @@ -9,18 +9,20 @@ import ( "gitea.dev/modules/git/gitcmd" ) +// TODO: all wrappers can be removed in next PR, because cmd now can accept Repository directly. + func RunCmd(ctx context.Context, repo Repository, cmd *gitcmd.Command) error { - return cmd.WithDir(repoPath(repo)).WithParentCallerInfo().Run(ctx) + return cmd.WithRepo(repo).WithParentCallerInfo().Run(ctx) } func RunCmdString(ctx context.Context, repo Repository, cmd *gitcmd.Command) (string, string, gitcmd.RunStdError) { - return cmd.WithDir(repoPath(repo)).WithParentCallerInfo().RunStdString(ctx) + return cmd.WithRepo(repo).WithParentCallerInfo().RunStdString(ctx) } func RunCmdBytes(ctx context.Context, repo Repository, cmd *gitcmd.Command) ([]byte, []byte, gitcmd.RunStdError) { - return cmd.WithDir(repoPath(repo)).WithParentCallerInfo().RunStdBytes(ctx) + return cmd.WithRepo(repo).WithParentCallerInfo().RunStdBytes(ctx) } func RunCmdWithStderr(ctx context.Context, repo Repository, cmd *gitcmd.Command) gitcmd.RunStdError { - return cmd.WithDir(repoPath(repo)).WithParentCallerInfo().RunWithStderr(ctx) + return cmd.WithRepo(repo).WithParentCallerInfo().RunWithStderr(ctx) } diff --git a/modules/gitrepo/commit.go b/modules/gitrepo/commit.go index 38785dcb97..24df90c015 100644 --- a/modules/gitrepo/commit.go +++ b/modules/gitrepo/commit.go @@ -32,6 +32,14 @@ func CommitsCount(ctx context.Context, repo Repository, opts CommitsCountOptions cmd.AddOptionValues("--not", opts.Not) } + if opts.Since != "" { + cmd.AddOptionFormat("--since=%s", opts.Since) + } + + if opts.Until != "" { + cmd.AddOptionFormat("--until=%s", opts.Until) + } + if len(opts.RelPath) > 0 { cmd.AddDashesAndList(opts.RelPath...) } diff --git a/modules/gitrepo/commit_file_test.go b/modules/gitrepo/commit_file_test.go index ec1018eeba..f0dd932082 100644 --- a/modules/gitrepo/commit_file_test.go +++ b/modules/gitrepo/commit_file_test.go @@ -116,7 +116,7 @@ func TestParseCommitFileStatus(t *testing.T) { } func TestGetCommitFileStatusMerges(t *testing.T) { - bareRepo6 := &mockRepository{path: "repo6_merge"} + bareRepo6 := mockRepository("repo6_merge") commitFileStatus, err := GetCommitFileStatus(t.Context(), bareRepo6, "022f4ce6214973e018f02bf363bf8a2e3691f699") assert.NoError(t, err) @@ -139,7 +139,7 @@ func TestGetCommitFileStatusMerges(t *testing.T) { } func TestGetCommitFileStatusMergesSha256(t *testing.T) { - bareRepo6Sha256 := &mockRepository{path: "repo6_merge_sha256"} + bareRepo6Sha256 := mockRepository("repo6_merge_sha256") commitFileStatus, err := GetCommitFileStatus(t.Context(), bareRepo6Sha256, "d2e5609f630dd8db500f5298d05d16def282412e3e66ed68cc7d0833b29129a1") assert.NoError(t, err) diff --git a/modules/gitrepo/commit_test.go b/modules/gitrepo/commit_test.go index 05cedc39ef..c742b17f9c 100644 --- a/modules/gitrepo/commit_test.go +++ b/modules/gitrepo/commit_test.go @@ -10,7 +10,7 @@ import ( ) func TestCommitsCount(t *testing.T) { - bareRepo1 := &mockRepository{path: "repo1_bare"} + bareRepo1 := mockRepository("repo1_bare") commitsCount, err := CommitsCount(t.Context(), bareRepo1, CommitsCountOptions{ @@ -21,8 +21,39 @@ func TestCommitsCount(t *testing.T) { assert.Equal(t, int64(3), commitsCount) } +func TestCommitsCountWithSinceUntil(t *testing.T) { + bareRepo1 := mockRepository("repo1_bare") + revision := []string{"8006ff9adbf0cb94da7dad9e537e53817f9fa5c0"} + + // The three commits on this revision are dated 2018-04-18, 2017-12-19 and 2017-12-19. + cases := []struct { + name string + since string + until string + expected int64 + }{ + {name: "no filter", expected: 3}, + {name: "since keeps newer commits", since: "2018-01-01", expected: 1}, + {name: "until keeps older commits", until: "2018-01-01", expected: 2}, + {name: "since and until bound the range", since: "2017-12-19T22:16:00-08:00", until: "2018-01-01", expected: 1}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + commitsCount, err := CommitsCount(t.Context(), bareRepo1, + CommitsCountOptions{ + Revision: revision, + Since: tc.since, + Until: tc.until, + }) + + assert.NoError(t, err) + assert.Equal(t, tc.expected, commitsCount) + }) + } +} + func TestCommitsCountWithoutBase(t *testing.T) { - bareRepo1 := &mockRepository{path: "repo1_bare"} + bareRepo1 := mockRepository("repo1_bare") commitsCount, err := CommitsCount(t.Context(), bareRepo1, CommitsCountOptions{ @@ -35,7 +66,7 @@ func TestCommitsCountWithoutBase(t *testing.T) { } func TestGetLatestCommitTime(t *testing.T) { - bareRepo1 := &mockRepository{path: "repo1_bare"} + bareRepo1 := mockRepository("repo1_bare") lct, err := GetLatestCommitTime(t.Context(), bareRepo1) assert.NoError(t, err) // Time is Sun Nov 13 16:40:14 2022 +0100 diff --git a/modules/gitrepo/commitgraph.go b/modules/gitrepo/commitgraph.go deleted file mode 100644 index adafd107bf..0000000000 --- a/modules/gitrepo/commitgraph.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package gitrepo - -import ( - "context" - - "gitea.dev/modules/git" -) - -func WriteCommitGraph(ctx context.Context, repo Repository) error { - return git.WriteCommitGraph(ctx, repoPath(repo)) -} diff --git a/modules/gitrepo/compare_test.go b/modules/gitrepo/compare_test.go index 770a3589ea..3808417ba5 100644 --- a/modules/gitrepo/compare_test.go +++ b/modules/gitrepo/compare_test.go @@ -15,14 +15,6 @@ import ( "github.com/stretchr/testify/require" ) -type mockRepository struct { - path string -} - -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())) @@ -44,13 +36,13 @@ data 12 Hello from 2 `))).RunStdString(t.Context()) require.NoError(t, runErr) - mergeBase, err := MergeBase(t.Context(), &mockRepository{path: repoDir}, "branch1", "branch2") + mergeBase, err := MergeBase(t.Context(), mockRepository(repoDir), "branch1", "branch2") assert.Empty(t, mergeBase) assert.ErrorIs(t, err, util.ErrNotExist) } func TestRepoGetDivergingCommits(t *testing.T) { - repo := &mockRepository{path: "repo1_bare"} + repo := mockRepository("repo1_bare") do, err := GetDivergingCommits(t.Context(), repo, "master", "branch2") assert.NoError(t, err) assert.Equal(t, &DivergeObject{ @@ -74,7 +66,7 @@ func TestRepoGetDivergingCommits(t *testing.T) { } func TestGetCommitIDsBetweenReverse(t *testing.T) { - repo := &mockRepository{path: "repo1_bare"} + repo := mockRepository("repo1_bare") // tests raw commit IDs commitIDs, err := GetCommitIDsBetweenReverse(t.Context(), repo, diff --git a/modules/gitrepo/config.go b/modules/gitrepo/config.go index a6e97d594d..56e3d0e82d 100644 --- a/modules/gitrepo/config.go +++ b/modules/gitrepo/config.go @@ -6,17 +6,13 @@ package gitrepo import ( "context" + "gitea.dev/modules/git" "gitea.dev/modules/git/gitcmd" - "gitea.dev/modules/globallock" ) -func getRepoConfigLockKey(repoStoragePath string) string { - return "repo-config:" + repoStoragePath -} - // GitConfigAdd add a git configuration key to a specific value for the given repository. func GitConfigAdd(ctx context.Context, repo Repository, key, value string) error { - return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error { + return git.LockConfigAndDo(ctx, repo, func(ctx context.Context) error { _, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("config", "--add"). AddDynamicArguments(key, value)) return err @@ -27,7 +23,7 @@ func GitConfigAdd(ctx context.Context, repo Repository, key, value string) error // If the key does not exist, it will be created. // If the key exists, it will be updated to the new value. func GitConfigSet(ctx context.Context, repo Repository, key, value string) error { - return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error { + return git.LockConfigAndDo(ctx, repo, func(ctx context.Context) error { _, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("config"). AddDynamicArguments(key, value)) return err diff --git a/modules/gitrepo/fetch.go b/modules/gitrepo/fetch.go index b50571e5b2..aae4349714 100644 --- a/modules/gitrepo/fetch.go +++ b/modules/gitrepo/fetch.go @@ -6,8 +6,8 @@ package gitrepo import ( "context" + "gitea.dev/modules/git" "gitea.dev/modules/git/gitcmd" - "gitea.dev/modules/globallock" ) // FetchRemoteCommit fetches a specific commit and its related objects from a remote @@ -20,7 +20,7 @@ import ( // This behavior is sufficient for temporary operations, such as determining the // merge base between commits. func FetchRemoteCommit(ctx context.Context, repo, remoteRepo Repository, commitID string) error { - return globallock.LockAndDo(ctx, getRepoWriteLockKey(repo.RelativePath()), func(ctx context.Context) error { + return git.LockWriteAndDo(ctx, repo, func(ctx context.Context) error { return RunCmd(ctx, repo, gitcmd.NewCommand("fetch", "--no-tags"). AddDynamicArguments(repoPath(remoteRepo)). AddDynamicArguments(commitID)) diff --git a/modules/gitrepo/gitrepo.go b/modules/gitrepo/gitrepo.go index 17eabb2aad..6574ca3234 100644 --- a/modules/gitrepo/gitrepo.go +++ b/modules/gitrepo/gitrepo.go @@ -14,29 +14,19 @@ import ( "gitea.dev/modules/git" "gitea.dev/modules/git/gitcmd" "gitea.dev/modules/reqctx" - "gitea.dev/modules/setting" "gitea.dev/modules/util" ) -// Repository represents a git repository which stored in a disk -type Repository interface { - RelativePath() string // We don't assume how the directory structure of the repository is, so we only need the relative path -} +type Repository = gitcmd.RepositoryFacade -// repoPath resolves the Repository.RelativePath (which is a unix-style path like "username/reponame.git") -// to a local filesystem path according to setting.RepoRootPath -var repoPath = func(repo Repository) string { - return filepath.Join(setting.RepoRootPath, filepath.FromSlash(repo.RelativePath())) -} - -// OpenRepository opens the repository at the given relative path with the provided context. -func OpenRepository(ctx context.Context, repo Repository) (*git.Repository, error) { - return git.OpenRepository(ctx, repoPath(repo)) -} +var ( + repoPath = gitcmd.RepoLocalPath + OpenRepository = git.OpenRepository +) // contextKey is a value for use with context.WithValue. type contextKey struct { - repoPath string + key string } // RepositoryFromContextOrOpen attempts to get the repository from the context or just opens it @@ -47,18 +37,18 @@ func RepositoryFromContextOrOpen(ctx context.Context, repo Repository) (*git.Rep gitRepo, err := RepositoryFromRequestContextOrOpen(reqCtx, repo) return gitRepo, util.NopCloser{}, err } - gitRepo, err := OpenRepository(ctx, repo) + gitRepo, err := OpenRepository(repo) return gitRepo, gitRepo, err } // RepositoryFromRequestContextOrOpen opens the repository at the given relative path in the provided request context. // Caller shouldn't close the git repo manually, the git repo will be automatically closed when the request context is done. func RepositoryFromRequestContextOrOpen(ctx reqctx.RequestContext, repo Repository) (*git.Repository, error) { - ck := contextKey{repoPath: repoPath(repo)} + ck := contextKey{key: repo.GitRepoLocation()} if gitRepo, ok := ctx.Value(ck).(*git.Repository); ok { return gitRepo, nil } - gitRepo, err := git.OpenRepository(ctx, ck.repoPath) + gitRepo, err := git.OpenRepository(repo) if err != nil { return nil, err } diff --git a/modules/gitrepo/main_test.go b/modules/gitrepo/main_test.go index d7dd292155..3dc79fd386 100644 --- a/modules/gitrepo/main_test.go +++ b/modules/gitrepo/main_test.go @@ -8,18 +8,19 @@ import ( "testing" "gitea.dev/modules/git" + "gitea.dev/modules/git/gitcmd" "gitea.dev/modules/setting" ) -func TestMain(m *testing.M) { - // resolve repository path relative to the test directory - setting.SetupGiteaTestEnv() - giteaRoot := setting.GetGiteaTestSourceRoot() - repoPath = func(repo Repository) string { - if filepath.IsAbs(repo.RelativePath()) { - return repo.RelativePath() // for testing purpose only - } - return filepath.Join(giteaRoot, "modules/git/tests/repos", repo.RelativePath()) +func mockRepository(repoPath string) gitcmd.RepositoryFacade { + if !filepath.IsAbs(repoPath) { + // resolve repository path relative to the unit test fixture directory + repoPath = filepath.Join(setting.GetGiteaTestSourceRoot(), "modules/git/tests/repos", repoPath) } + return gitcmd.RepositoryManaged(repoPath, repoPath) +} + +func TestMain(m *testing.M) { + setting.SetupGiteaTestEnv() git.RunGitTests(m) } diff --git a/modules/gitrepo/merge_tree_test.go b/modules/gitrepo/merge_tree_test.go index 7d1075fd7b..89f689e801 100644 --- a/modules/gitrepo/merge_tree_test.go +++ b/modules/gitrepo/merge_tree_test.go @@ -69,7 +69,7 @@ M 100644 :5 z/d func TestMergeTreeDirectoryRenameConflictWithoutFiles(t *testing.T) { repoDir := prepareRepoDirRenameConflict(t) require.DirExists(t, repoDir) - repo := &mockRepository{path: repoDir} + repo := mockRepository(repoDir) mergeBase, err := MergeBase(t.Context(), repo, "add", "split") require.NoError(t, err) diff --git a/modules/gitrepo/remote.go b/modules/gitrepo/remote.go index eeef123ba7..96a68d2fdd 100644 --- a/modules/gitrepo/remote.go +++ b/modules/gitrepo/remote.go @@ -10,7 +10,6 @@ import ( "gitea.dev/modules/git" "gitea.dev/modules/git/gitcmd" giturl "gitea.dev/modules/git/url" - "gitea.dev/modules/globallock" "gitea.dev/modules/util" ) @@ -22,7 +21,7 @@ const ( ) func GitRemoteAdd(ctx context.Context, repo Repository, remoteName, remoteURL string, options ...RemoteOption) error { - return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error { + return git.LockConfigAndDo(ctx, repo, func(ctx context.Context) error { cmd := gitcmd.NewCommand("remote", "add") if len(options) > 0 { switch options[0] { @@ -40,7 +39,7 @@ func GitRemoteAdd(ctx context.Context, repo Repository, remoteName, remoteURL st } func GitRemoteRemove(ctx context.Context, repo Repository, remoteName string) error { - return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error { + return git.LockConfigAndDo(ctx, repo, func(ctx context.Context) error { cmd := gitcmd.NewCommand("remote", "rm").AddDynamicArguments(remoteName) _, _, err := RunCmdString(ctx, repo, cmd) return err diff --git a/modules/gitrepo/repo_lock.go b/modules/gitrepo/repo_lock.go deleted file mode 100644 index 2eb89ce807..0000000000 --- a/modules/gitrepo/repo_lock.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2026 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package gitrepo - -// getRepoWriteLockKey returns the global lock key for write operations on the repository. -// Parallel write operations on the same git repository should be avoided to prevent data corruption. -func getRepoWriteLockKey(repoStoragePath string) string { - return "repo-write:" + repoStoragePath -} diff --git a/modules/gitrepo/signing.go b/modules/gitrepo/signing.go deleted file mode 100644 index aa8f947e99..0000000000 --- a/modules/gitrepo/signing.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package gitrepo - -import ( - "context" - - "gitea.dev/modules/git" -) - -func GetSigningKey(ctx context.Context) (*git.SigningKey, *git.Signature) { - return git.GetSigningKey(ctx) -} diff --git a/modules/gitrepo/url.go b/modules/gitrepo/url.go deleted file mode 100644 index b355d0fa93..0000000000 --- a/modules/gitrepo/url.go +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package gitrepo - -func RepoGitURL(repo Repository) string { - return repoPath(repo) -} diff --git a/modules/gitrepo/walk_gogit.go b/modules/gitrepo/walk_gogit.go deleted file mode 100644 index 709897ba0c..0000000000 --- a/modules/gitrepo/walk_gogit.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build gogit - -package gitrepo - -import ( - "context" - - "github.com/go-git/go-git/v5/plumbing" -) - -// WalkReferences walks all the references from the repository -// refname is empty, ObjectTag or ObjectBranch. All other values should be treated as equivalent to empty. -func WalkReferences(ctx context.Context, repo Repository, walkfn func(sha1, refname string) error) (int, error) { - gitRepo, closer, err := RepositoryFromContextOrOpen(ctx, repo) - if err != nil { - return 0, err - } - defer closer.Close() - - i := 0 - iter, err := gitRepo.GoGitRepo().References() - if err != nil { - return i, err - } - defer iter.Close() - - err = iter.ForEach(func(ref *plumbing.Reference) error { - err := walkfn(ref.Hash().String(), string(ref.Name())) - i++ - return err - }) - return i, err -} diff --git a/modules/gitrepo/walk_nogogit.go b/modules/gitrepo/walk_nogogit.go deleted file mode 100644 index b4f6f7dfc4..0000000000 --- a/modules/gitrepo/walk_nogogit.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build !gogit - -package gitrepo - -import ( - "context" - - "gitea.dev/modules/git" -) - -// WalkReferences walks all the references from the repository -func WalkReferences(ctx context.Context, repo Repository, walkfn func(sha1, refname string) error) (int, error) { - return git.WalkShowRef(ctx, repoPath(repo), nil, 0, 0, walkfn) -} diff --git a/modules/hostmatcher/hostmatcher.go b/modules/hostmatcher/hostmatcher.go index 7c17bc95da..c82a57b7b9 100644 --- a/modules/hostmatcher/hostmatcher.go +++ b/modules/hostmatcher/hostmatcher.go @@ -65,12 +65,9 @@ var reservedIPNets = sync.OnceValue(func() []*net.IPNet { return nets }) -// isPrivateIP reports whether ip falls in a private (net.IP.IsPrivate) or reserved special-purpose +// isReservedIP reports whether ip falls in reserved special-purpose // range (see reservedIPNets) that must not be considered a public/external destination. -func isPrivateIP(ip net.IP) bool { - if ip.IsPrivate() { - return true - } +func isReservedIP(ip net.IP) bool { for _, ipNet := range reservedIPNets() { if ipNet.Contains(ip) { return true @@ -148,18 +145,22 @@ func (hl *HostMatchList) checkPattern(host string) bool { return false } -func (hl *HostMatchList) checkIP(ip net.IP) bool { +// matchesIP determines if the given IP matches any of the configured rules +func (hl *HostMatchList) matchesIP(ip net.IP) bool { if slices.Contains(hl.patterns, "*") { return true } for _, builtin := range hl.builtins { switch builtin { case MatchBuiltinExternal: - if ip.IsGlobalUnicast() && !isPrivateIP(ip) { + // External address must be a global unicast, must not be in reserved range and must not be in private range + if ip.IsGlobalUnicast() && !isReservedIP(ip) && !ip.IsPrivate() { return true } case MatchBuiltinPrivate: - if isPrivateIP(ip) { + // Private address must be global unicast, must not be in range we explicitly exclude for security reasons + // and must be in private range + if ip.IsGlobalUnicast() && !isReservedIP(ip) && ip.IsPrivate() { return true } case MatchBuiltinLoopback: @@ -190,7 +191,7 @@ func (hl *HostMatchList) MatchHostName(host string) bool { return true } if ip := net.ParseIP(hostname); ip != nil { - return hl.checkIP(ip) + return hl.matchesIP(ip) } return false } @@ -201,7 +202,7 @@ func (hl *HostMatchList) MatchIPAddr(ip net.IP) bool { return false } host := ip.String() // nil-safe, we will get "" if ip is nil - return hl.checkPattern(host) || hl.checkIP(ip) + return hl.checkPattern(host) || hl.matchesIP(ip) } // MatchHostOrIP checks if the host or IP matches an allow/deny(block) list diff --git a/modules/hostmatcher/hostmatcher_test.go b/modules/hostmatcher/hostmatcher_test.go index 61582f28d3..464354ff41 100644 --- a/modules/hostmatcher/hostmatcher_test.go +++ b/modules/hostmatcher/hostmatcher_test.go @@ -202,15 +202,17 @@ func TestReservedRanges(t *testing.T) { "198.18.0.1", // benchmarking "198.51.100.1", // TEST-NET-2 "203.0.113.1", // TEST-NET-3 + "169.254.169.254", // Cloud metadata "192.88.99.1", // 6to4 relay anycast "64:ff9b::1", // NAT64 "64:ff9b::a9fe:a9fe", // NAT64 embedding 169.254.169.254 "2001::1", // Teredo "2002::1", // 6to4 "2001:db8::1", // documentation + "fe80::1", // link local address } { addr := net.ParseIP(ip) assert.Falsef(t, external.MatchIPAddr(addr), "reserved ip %s must not be external", ip) - assert.Truef(t, private.MatchIPAddr(addr), "reserved ip %s should match private block-list", ip) + assert.Falsef(t, private.MatchIPAddr(addr), "reserved ip %s should match private block-list", ip) } } diff --git a/modules/hostmatcher/http.go b/modules/hostmatcher/http.go index 8828902034..1ee9893c24 100644 --- a/modules/hostmatcher/http.go +++ b/modules/hostmatcher/http.go @@ -5,8 +5,10 @@ package hostmatcher import ( "context" + "crypto/tls" "fmt" "net" + "net/http" "net/url" "syscall" "time" @@ -63,3 +65,17 @@ func NewDialContext(usage string, allowList, blockList *HostMatchList, proxy *ur return dialer.DialContext(ctx, network, addrOrHost) } } + +// NewHTTPTransport builds an http.Transport that validates the request target against the allow/block +// lists on the direct-dial path (DialContext). When an HTTP proxy is configured the proxy resolves and +// dials the target itself, so restricting the proxied target is the proxy server's responsibility, not +// Gitea's. proxyFunc selects the proxy URL per request (the http.Transport.Proxy selector, e.g. +// proxy.Proxy()); proxyURLFixed is the fixed proxy address the dialer must always permit; tlsConfig may +// be nil. blockList may be nil for callers that only maintain an allow-list. +func NewHTTPTransport(usage string, allowList, blockList *HostMatchList, proxyFunc func(*http.Request) (*url.URL, error), proxyURLFixed *url.URL, tlsConfig *tls.Config) *http.Transport { + return &http.Transport{ + TLSClientConfig: tlsConfig, + Proxy: proxyFunc, + DialContext: NewDialContext(usage, allowList, blockList, proxyURLFixed), + } +} diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go index ed5f38fcf0..46ef2533f5 100644 --- a/modules/indexer/code/bleve/bleve.go +++ b/modules/indexer/code/bleve/bleve.go @@ -212,7 +212,7 @@ func (b *Indexer) addDelete(filename string, repo *repo_model.Repository, batch func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error { batch := inner_bleve.NewFlushingBatch(b.inner.Indexer, maxBatchSize) if len(changes.Updates) > 0 { - catfileBatch, err := gitrepo.NewBatch(ctx, repo) + catfileBatch, err := git.NewBatch(ctx, repo) if err != nil { return err } diff --git a/modules/indexer/code/elasticsearch/elasticsearch.go b/modules/indexer/code/elasticsearch/elasticsearch.go index 4c35a17b05..bc1aed1a22 100644 --- a/modules/indexer/code/elasticsearch/elasticsearch.go +++ b/modules/indexer/code/elasticsearch/elasticsearch.go @@ -183,7 +183,7 @@ func (b *Indexer) addDelete(filename string, repo *repo_model.Repository) es.Bul func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error { ops := make([]es.BulkOp, 0) if len(changes.Updates) > 0 { - batch, err := gitrepo.NewBatch(ctx, repo) + batch, err := git.NewBatch(ctx, repo) if err != nil { return err } diff --git a/modules/indexer/code/git.go b/modules/indexer/code/git.go index 88a313c33e..e1ca1ff4b9 100644 --- a/modules/indexer/code/git.go +++ b/modules/indexer/code/git.go @@ -26,7 +26,7 @@ func getDefaultBranchSha(ctx context.Context, repo *repo_model.Repository) (stri } // getRepoChanges returns changes to repo since last indexer update -func getRepoChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*internal.RepoChanges, error) { +func getRepoChanges(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, revision string) (*internal.RepoChanges, error) { status, err := repo_model.GetIndexerStatus(ctx, repo, repo_model.RepoIndexerTypeCode) if err != nil { return nil, err @@ -40,9 +40,9 @@ func getRepoChanges(ctx context.Context, repo *repo_model.Repository, revision s } if needGenesis { - return genesisChanges(ctx, repo, revision) + return genesisChanges(ctx, repo, gitRepo, revision) } - return nonGenesisChanges(ctx, repo, revision) + return nonGenesisChanges(ctx, repo, gitRepo, revision) } func isIndexable(entry *git.TreeEntry) bool { @@ -64,7 +64,7 @@ func isIndexable(entry *git.TreeEntry) bool { } // parseGitLsTreeOutput parses the output of a `git ls-tree -r --full-name` command -func parseGitLsTreeOutput(stdout []byte) ([]internal.FileUpdate, error) { +func parseGitLsTreeOutput(ctx context.Context, gitRepo *git.Repository, stdout []byte) ([]internal.FileUpdate, error) { entries, err := git.ParseTreeEntries(stdout) if err != nil { return nil, err @@ -76,7 +76,7 @@ func parseGitLsTreeOutput(stdout []byte) ([]internal.FileUpdate, error) { updates[idxCount] = internal.FileUpdate{ Filename: entry.Name(), BlobSha: entry.ID.String(), - Size: entry.Size(), + Size: entry.GetSize(ctx, gitRepo), Sized: true, } idxCount++ @@ -86,7 +86,7 @@ func parseGitLsTreeOutput(stdout []byte) ([]internal.FileUpdate, error) { } // genesisChanges get changes to add repo to the indexer for the first time -func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*internal.RepoChanges, error) { +func genesisChanges(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, revision string) (*internal.RepoChanges, error) { var changes internal.RepoChanges stdout, _, runErr := gitrepo.RunCmdBytes(ctx, repo, gitcmd.NewCommand("ls-tree", "--full-tree", "-l", "-r").AddDynamicArguments(revision)) if runErr != nil { @@ -94,12 +94,12 @@ func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision s } var err error - changes.Updates, err = parseGitLsTreeOutput(stdout) + changes.Updates, err = parseGitLsTreeOutput(ctx, gitRepo, stdout) return &changes, err } // nonGenesisChanges get changes since the previous indexer update -func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*internal.RepoChanges, error) { +func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, revision string) (*internal.RepoChanges, error) { diffCmd := gitcmd.NewCommand("diff", "--name-status").AddDynamicArguments(repo.CodeIndexerStatus.CommitSha, revision) stdout, _, runErr := gitrepo.RunCmdString(ctx, repo, diffCmd) if runErr != nil { @@ -109,7 +109,7 @@ func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revisio if err := (*globalIndexer.Load()).Delete(ctx, repo.ID); err != nil { return nil, err } - return genesisChanges(ctx, repo, revision) + return genesisChanges(ctx, repo, gitRepo, revision) } var changes internal.RepoChanges @@ -124,7 +124,7 @@ func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revisio return err } - updates, err1 := parseGitLsTreeOutput(lsTreeStdout) + updates, err1 := parseGitLsTreeOutput(ctx, gitRepo, lsTreeStdout) if err1 != nil { return err1 } diff --git a/modules/indexer/code/gitgrep/gitgrep.go b/modules/indexer/code/gitgrep/gitgrep.go index 1d295098d8..a3ad6cbdca 100644 --- a/modules/indexer/code/gitgrep/gitgrep.go +++ b/modules/indexer/code/gitgrep/gitgrep.go @@ -42,7 +42,7 @@ func PerformSearch(ctx context.Context, page int, repoID int64, gitRepo *git.Rep // TODO: if no branch exists, it reports: exit status 128, fatal: this operation must be run in a work tree. return nil, 0, fmt.Errorf("git.GrepSearch: %w", err) } - commitID, err := gitRepo.GetRefCommitID(ref.String()) + commitID, err := gitRepo.GetRefCommitID(ctx, ref.String()) if err != nil { return nil, 0, fmt.Errorf("gitRepo.GetRefCommitID: %w", err) } diff --git a/modules/indexer/code/indexer.go b/modules/indexer/code/indexer.go index 312b038e0e..be06712815 100644 --- a/modules/indexer/code/indexer.go +++ b/modules/indexer/code/indexer.go @@ -13,6 +13,7 @@ import ( "gitea.dev/models/db" repo_model "gitea.dev/models/repo" + "gitea.dev/modules/gitrepo" "gitea.dev/modules/graceful" "gitea.dev/modules/indexer" "gitea.dev/modules/indexer/code/bleve" @@ -73,11 +74,17 @@ func index(ctx context.Context, indexer internal.Indexer, repoID int64) error { return nil } + gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) + if err != nil { + return err + } + defer closer.Close() + sha, err := getDefaultBranchSha(ctx, repo) if err != nil { return err } - changes, err := getRepoChanges(ctx, repo, sha) + changes, err := getRepoChanges(ctx, repo, gitRepo, sha) if err != nil { return err } else if changes == nil { diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go index a919317728..25dbdebfa4 100644 --- a/modules/indexer/issues/bleve/bleve.go +++ b/modules/indexer/issues/bleve/bleve.go @@ -11,7 +11,6 @@ import ( indexer_internal "gitea.dev/modules/indexer/internal" inner_bleve "gitea.dev/modules/indexer/internal/bleve" "gitea.dev/modules/indexer/issues/internal" - "gitea.dev/modules/optional" "gitea.dev/modules/util" "github.com/blevesearch/bleve/v2" @@ -27,7 +26,7 @@ import ( const ( issueIndexerAnalyzer = "issueIndexer" issueIndexerDocType = "issueIndexerDocType" - issueIndexerLatestVersion = 6 + issueIndexerLatestVersion = 7 ) const unicodeNormalizeName = "unicodeNormalize" @@ -86,7 +85,8 @@ func generateIssueIndexMapping() (mapping.IndexMapping, error) { docMapping.AddFieldMappingsAt("project_ids", numberFieldMapping) docMapping.AddFieldMappingsAt("no_project", boolFieldMapping) docMapping.AddFieldMappingsAt("poster_id", numberFieldMapping) - docMapping.AddFieldMappingsAt("assignee_id", numberFieldMapping) + docMapping.AddFieldMappingsAt("assignee_ids", numberFieldMapping) + docMapping.AddFieldMappingsAt("no_assignee", boolFieldMapping) docMapping.AddFieldMappingsAt("mention_ids", numberFieldMapping) docMapping.AddFieldMappingsAt("reviewed_ids", numberFieldMapping) docMapping.AddFieldMappingsAt("review_requested_ids", numberFieldMapping) @@ -258,14 +258,15 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( queries = append(queries, inner_bleve.NumericEqualityQuery(posterIDInt64, "poster_id")) } - if options.AssigneeID != "" { - if options.AssigneeID == "(any)" { - queries = append(queries, inner_bleve.NumericRangeInclusiveQuery(optional.Some[int64](1), optional.None[int64](), "assignee_id")) - } else { - // "(none)" becomes 0, it means no assignee - assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64) - queries = append(queries, inner_bleve.NumericEqualityQuery(assigneeIDInt64, "assignee_id")) - } + switch options.AssigneeID { + case "": + case "(any)": + queries = append(queries, inner_bleve.BoolFieldQuery(false, "no_assignee")) + case "(none)": + queries = append(queries, inner_bleve.BoolFieldQuery(true, "no_assignee")) + default: + assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64) + queries = append(queries, inner_bleve.NumericEqualityQuery(assigneeIDInt64, "assignee_ids")) } if options.MentionID.Has() { diff --git a/modules/indexer/issues/bleve/bleve_test.go b/modules/indexer/issues/bleve/bleve_test.go index ccd7ab7b92..7dc664e0ef 100644 --- a/modules/indexer/issues/bleve/bleve_test.go +++ b/modules/indexer/issues/bleve/bleve_test.go @@ -6,7 +6,11 @@ package bleve import ( "testing" + "gitea.dev/modules/indexer/issues/internal" "gitea.dev/modules/indexer/issues/internal/tests" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestBleveIndexer(t *testing.T) { @@ -16,3 +20,67 @@ func TestBleveIndexer(t *testing.T) { tests.TestIndexer(t, indexer) } + +func TestBleveIndexerNoAssignee(t *testing.T) { + dir := t.TempDir() + indexer := NewIndexer(dir) + defer indexer.Close() + + _, err := indexer.Init(t.Context()) + require.NoError(t, err) + + require.NoError(t, indexer.Index(t.Context(), + &internal.IndexerData{ID: 1, Title: "assigned through assignee_ids", AssigneeIDs: []int64{2}}, + &internal.IndexerData{ID: 2, Title: "unassigned", NoAssignee: true}, + &internal.IndexerData{ID: 3, Title: "assigned through multiple assignee_ids", AssigneeIDs: []int64{3, 4}}, + )) + + testCases := []struct { + name string + opts *internal.SearchOptions + expectedIDs []int64 + }{ + { + name: "none", + opts: &internal.SearchOptions{AssigneeID: "(none)"}, + expectedIDs: []int64{2}, + }, + { + name: "any", + opts: &internal.SearchOptions{AssigneeID: "(any)"}, + expectedIDs: []int64{1, 3}, + }, + { + name: "specific", + opts: &internal.SearchOptions{AssigneeID: "2"}, + expectedIDs: []int64{1}, + }, + { + name: "specific first multi-assignee", + opts: &internal.SearchOptions{AssigneeID: "3"}, + expectedIDs: []int64{3}, + }, + { + name: "specific second multi-assignee", + opts: &internal.SearchOptions{AssigneeID: "4"}, + expectedIDs: []int64{3}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + result, err := indexer.Search(t.Context(), testCase.opts) + require.NoError(t, err) + assert.Equal(t, int64(len(testCase.expectedIDs)), result.Total) + assert.ElementsMatch(t, testCase.expectedIDs, searchResultIDs(result)) + }) + } +} + +func searchResultIDs(result *internal.SearchResult) []int64 { + ids := make([]int64, 0, len(result.Hits)) + for _, hit := range result.Hits { + ids = append(ids, hit.ID) + } + return ids +} diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go index baef644cc1..87670a428d 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch.go @@ -16,7 +16,7 @@ import ( "gitea.dev/modules/util" ) -const issueIndexerLatestVersion = 3 +const issueIndexerLatestVersion = 4 var _ internal.Indexer = &Indexer{} @@ -57,7 +57,8 @@ const ( "project_ids": { "type": "integer", "index": true }, "no_project": { "type": "boolean", "index": true }, "poster_id": { "type": "integer", "index": true }, - "assignee_id": { "type": "integer", "index": true }, + "assignee_ids": { "type": "integer", "index": true }, + "no_assignee": { "type": "boolean", "index": true }, "mention_ids": { "type": "integer", "index": true }, "reviewed_ids": { "type": "integer", "index": true }, "review_requested_ids": { "type": "integer", "index": true }, @@ -177,14 +178,15 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query.Must(es.TermQuery("poster_id", posterIDInt64)) } - if options.AssigneeID != "" { - if options.AssigneeID == "(any)" { - 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(es.TermQuery("assignee_id", assigneeIDInt64)) - } + switch options.AssigneeID { + case "": + case "(any)": + query.Must(es.TermQuery("no_assignee", false)) + case "(none)": + query.Must(es.TermQuery("no_assignee", true)) + default: + assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64) + query.Must(es.TermQuery("assignee_ids", assigneeIDInt64)) } if options.MentionID.Has() { diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index b964f349df..0e9162f0b9 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -34,7 +34,8 @@ type IndexerData struct { 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"` + AssigneeIDs []int64 `json:"assignee_ids"` + NoAssignee bool `json:"no_assignee"` // True if the issue has no assignees MentionIDs []int64 `json:"mention_ids"` ReviewedIDs []int64 `json:"reviewed_ids"` ReviewRequestedIDs []int64 `json:"review_requested_ids"` diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index 276f3fb5bb..8337272592 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -377,10 +377,10 @@ var cases = []*testIndexerCase{ 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].AssigneeID) + assert.True(t, slices.Contains(data[v.ID].AssigneeIDs, int64(1))) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.AssigneeID == 1 + return slices.Contains(v.AssigneeIDs, int64(1)) }), result.Total) }, }, @@ -395,10 +395,10 @@ var cases = []*testIndexerCase{ 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].AssigneeID) + assert.True(t, data[v.ID].NoAssignee) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.AssigneeID == 0 + return v.NoAssignee }), result.Total) }, }, @@ -630,10 +630,10 @@ var cases = []*testIndexerCase{ Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Len(t, result.Hits, 180) for _, v := range result.Hits { - assert.GreaterOrEqual(t, data[v.ID].AssigneeID, int64(1)) + assert.False(t, data[v.ID].NoAssignee) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.AssigneeID >= 1 + return !v.NoAssignee }), result.Total) }, }, @@ -686,6 +686,18 @@ func generateDefaultIndexerData() []*internal.IndexerData { for i := range projectIDs { projectIDs[i] = int64(i) + 1 // projectID should not be 0 } + var assigneeIDs []int64 + if issueIndex%10 != 0 { + assigneeID := issueIndex % 10 + assigneeIDs = []int64{assigneeID} + if issueIndex%3 == 0 { + nextAssigneeID := assigneeID + 1 + if nextAssigneeID == 10 { + nextAssigneeID = 1 + } + assigneeIDs = append(assigneeIDs, nextAssigneeID) + } + } data = append(data, &internal.IndexerData{ ID: id, @@ -702,7 +714,8 @@ func generateDefaultIndexerData() []*internal.IndexerData { ProjectIDs: projectIDs, NoProject: len(projectIDs) == 0, PosterID: id%10 + 1, // PosterID should not be 0 - AssigneeID: issueIndex % 10, + AssigneeIDs: assigneeIDs, + NoAssignee: len(assigneeIDs) == 0, MentionIDs: mentionIDs, ReviewedIDs: reviewedIDs, ReviewRequestedIDs: reviewRequestedIDs, diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go index ddde9c89f0..1ec55bc7e2 100644 --- a/modules/indexer/issues/meilisearch/meilisearch.go +++ b/modules/indexer/issues/meilisearch/meilisearch.go @@ -20,7 +20,7 @@ import ( ) const ( - issueIndexerLatestVersion = 5 + issueIndexerLatestVersion = 6 // TODO: make this configurable if necessary maxTotalHits = 10000 @@ -74,7 +74,8 @@ func NewIndexer(url, apiKey, indexerName string) *Indexer { "project_ids", "no_project", "poster_id", - "assignee_id", + "assignee_ids", + "no_assignee", "mention_ids", "reviewed_ids", "review_requested_ids", @@ -195,14 +196,15 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query.And(inner_meilisearch.NewFilterEq("poster_id", posterIDInt64)) } - if options.AssigneeID != "" { - if options.AssigneeID == "(any)" { - query.And(inner_meilisearch.NewFilterGte("assignee_id", 1)) - } else { - // "(none)" becomes 0, it means no assignee - assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64) - query.And(inner_meilisearch.NewFilterEq("assignee_id", assigneeIDInt64)) - } + switch options.AssigneeID { + case "": + case "(any)": + query.And(inner_meilisearch.NewFilterEq("no_assignee", false)) + case "(none)": + query.And(inner_meilisearch.NewFilterEq("no_assignee", true)) + default: + assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64) + query.And(inner_meilisearch.NewFilterEq("assignee_ids", assigneeIDInt64)) } if options.MentionID.Has() { diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go index bbfd42be6b..993356c0b4 100644 --- a/modules/indexer/issues/util.go +++ b/modules/indexer/issues/util.go @@ -87,6 +87,11 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD return nil, false, err } + assigneeIDs := make([]int64, 0, len(issue.Assignees)) + for _, a := range issue.Assignees { + assigneeIDs = append(assigneeIDs, a.ID) + } + projectIDs := make([]int64, 0, len(issue.Projects)) for _, project := range issue.Projects { projectIDs = append(projectIDs, project.ID) @@ -112,7 +117,8 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD ProjectIDs: projectIDs, NoProject: len(projectIDs) == 0, PosterID: issue.PosterID, - AssigneeID: issue.AssigneeID, + AssigneeIDs: assigneeIDs, + NoAssignee: len(assigneeIDs) == 0, MentionIDs: mentionIDs, ReviewedIDs: reviewedIDs, ReviewRequestedIDs: reviewRequestedIDs, diff --git a/modules/indexer/stats/db.go b/modules/indexer/stats/db.go index af5127b638..e4ba9cf2e8 100644 --- a/modules/indexer/stats/db.go +++ b/modules/indexer/stats/db.go @@ -37,7 +37,7 @@ func (db *DBIndexer) Index(id int64) error { return err } - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + gitRepo, err := gitrepo.OpenRepository(repo) if err != nil { if err.Error() == "no such file or directory" { return nil @@ -47,7 +47,7 @@ func (db *DBIndexer) Index(id int64) error { defer gitRepo.Close() // Get latest commit for default branch - commitID, err := gitRepo.GetBranchCommitID(repo.DefaultBranch) + commitID, err := gitRepo.GetBranchCommitID(ctx, repo.DefaultBranch) if err != nil { if git.IsErrBranchNotExist(err) || git.IsErrNotExist(err) || setting.IsInTesting { log.Debug("Unable to get commit ID for default branch %s in %s ... skipping this repository", repo.DefaultBranch, repo.FullName()) @@ -63,7 +63,7 @@ func (db *DBIndexer) Index(id int64) error { } // Calculate and save language statistics to database - stats, err := languagestats.GetLanguageStats(gitRepo, commitID) + stats, err := languagestats.GetLanguageStats(ctx, gitRepo, commitID) if err != nil { if !setting.IsInTesting { log.Error("Unable to get language stats for ID %s for default branch %s in %s. Error: %v", commitID, repo.DefaultBranch, repo.FullName(), err) diff --git a/modules/issue/template/template.go b/modules/issue/template/template.go index 63f156f9f9..6f2372021d 100644 --- a/modules/issue/template/template.go +++ b/modules/issue/template/template.go @@ -167,7 +167,7 @@ func validateOptions(field *api.IssueFormField, idx int) error { options, ok := field.Attributes["options"].([]any) if !ok || len(options) == 0 { - return position.Errorf("'options' is required and should be a array") + return position.Errorf("'options' is required and should be an array") } for optIdx, option := range options { @@ -270,7 +270,7 @@ func validateDropdownDefault(position errorPosition, attributes map[string]any) options, ok := attributes["options"].([]any) if !ok { // should not happen - return position.Errorf("'options' is required and should be a array") + return position.Errorf("'options' is required and should be an array") } if defaultValue < 0 || defaultValue >= len(options) { return position.Errorf("the value of 'default' is out of range") diff --git a/modules/issue/template/template_test.go b/modules/issue/template/template_test.go index b696d952c8..310e383123 100644 --- a/modules/issue/template/template_test.go +++ b/modules/issue/template/template_test.go @@ -268,7 +268,7 @@ body: attributes: label: "a" `, - wantErr: "body[0](dropdown): 'options' is required and should be a array", + wantErr: "body[0](dropdown): 'options' is required and should be an array", }, { name: "dropdown invalid options", diff --git a/modules/issue/template/unmarshal.go b/modules/issue/template/unmarshal.go index e0d6881cf5..a8346885dd 100644 --- a/modules/issue/template/unmarshal.go +++ b/modules/issue/template/unmarshal.go @@ -4,6 +4,7 @@ package template import ( + "context" "fmt" "path" "strconv" @@ -41,35 +42,35 @@ func Unmarshal(filename string, content []byte) (*api.IssueTemplate, error) { } // UnmarshalFromEntry parses out a valid template from the blob in entry -func UnmarshalFromEntry(entry *git.TreeEntry, dir string) (*api.IssueTemplate, error) { - return unmarshalFromEntry(entry, path.Join(dir, entry.Name())) // Filepaths in Git are ALWAYS '/' separated do not use filepath here +func UnmarshalFromEntry(ctx context.Context, gitRepo *git.Repository, entry *git.TreeEntry, dir string) (*api.IssueTemplate, error) { + return unmarshalFromEntry(ctx, gitRepo, entry, path.Join(dir, entry.Name())) // Filepaths in Git are ALWAYS '/' separated do not use filepath here } // UnmarshalFromCommit parses out a valid template from the commit -func UnmarshalFromCommit(commit *git.Commit, filename string) (*api.IssueTemplate, error) { - entry, err := commit.GetTreeEntryByPath(filename) +func UnmarshalFromCommit(ctx context.Context, gitRepo *git.Repository, commit *git.Commit, filename string) (*api.IssueTemplate, error) { + entry, err := commit.GetTreeEntryByPath(ctx, gitRepo, filename) if err != nil { return nil, fmt.Errorf("get entry for %q: %w", filename, err) } - return unmarshalFromEntry(entry, filename) + return unmarshalFromEntry(ctx, gitRepo, entry, filename) } // UnmarshalFromRepo parses out a valid template from the head commit of the branch -func UnmarshalFromRepo(repo *git.Repository, branch, filename string) (*api.IssueTemplate, error) { - commit, err := repo.GetBranchCommit(branch) +func UnmarshalFromRepo(ctx context.Context, repo *git.Repository, branch, filename string) (*api.IssueTemplate, error) { + commit, err := repo.GetBranchCommit(ctx, branch) if err != nil { return nil, fmt.Errorf("get commit on branch %q: %w", branch, err) } - return UnmarshalFromCommit(commit, filename) + return UnmarshalFromCommit(ctx, repo, commit, filename) } -func unmarshalFromEntry(entry *git.TreeEntry, filename string) (*api.IssueTemplate, error) { - if size := entry.Blob().Size(); size > setting.UI.MaxDisplayFileSize { +func unmarshalFromEntry(ctx context.Context, gitRepo *git.Repository, entry *git.TreeEntry, filename string) (*api.IssueTemplate, error) { + if size := entry.Blob(gitRepo).Size(ctx); size > setting.UI.MaxDisplayFileSize { return nil, fmt.Errorf("too large: %v > MaxDisplayFileSize", size) } - r, err := entry.Blob().DataAsync() + r, err := entry.Blob(gitRepo).DataAsync(ctx) if err != nil { return nil, fmt.Errorf("data async: %w", err) } diff --git a/modules/lfstransfer/backend/backend.go b/modules/lfstransfer/backend/backend.go index 42a01bf492..6dde323706 100644 --- a/modules/lfstransfer/backend/backend.go +++ b/modules/lfstransfer/backend/backend.go @@ -40,13 +40,12 @@ type GiteaBackend struct { logger transfer.Logger } -func New(ctx context.Context, repo, op, token string, logger transfer.Logger) (transfer.Backend, error) { - // runServ guarantees repo will be in form [owner]/[name].git +func New(ctx context.Context, reqPath, op, token string, logger transfer.Logger) (transfer.Backend, error) { server, err := url.Parse(setting.LocalURL) if err != nil { return nil, err } - server = server.JoinPath("api/internal/repo", repo, "info/lfs") + server = server.JoinPath(reqPath) return &GiteaBackend{ctx: ctx, server: server, op: op, authToken: token, internalAuth: "Bearer " + setting.InternalToken, logger: logger}, nil } diff --git a/modules/lfstransfer/main.go b/modules/lfstransfer/main.go index e5c8a122a4..503936f245 100644 --- a/modules/lfstransfer/main.go +++ b/modules/lfstransfer/main.go @@ -6,6 +6,7 @@ package lfstransfer import ( "context" "fmt" + "net/url" "os" "gitea.dev/modules/lfstransfer/backend" @@ -13,23 +14,24 @@ import ( "github.com/charmbracelet/git-lfs-transfer/transfer" ) -func Main(ctx context.Context, repo, verb, token string) error { +func Main(ctx context.Context, ownerName, repoName, verb, token string) error { logger := newLogger() - pktline := transfer.NewPktline(os.Stdin, os.Stdout, logger) - giteaBackend, err := backend.New(ctx, repo, verb, token, logger) + backendReqPath := fmt.Sprintf("api/internal/repo/%s/%s.git/info/lfs", url.PathEscape(ownerName), url.PathEscape(repoName)) + giteaBackend, err := backend.New(ctx, backendReqPath, verb, token, logger) if err != nil { return err } + pktLine := transfer.NewPktline(os.Stdin, os.Stdout, logger) for _, cap := range backend.Capabilities { - if err := pktline.WritePacketText(cap); err != nil { + if err := pktLine.WritePacketText(cap); err != nil { logger.Log("error sending capability due to error:", err) } } - if err := pktline.WriteFlush(); err != nil { + if err := pktLine.WriteFlush(); err != nil { logger.Log("error flushing capabilities:", err) } - p := transfer.NewProcessor(pktline, giteaBackend, logger) + p := transfer.NewProcessor(pktLine, giteaBackend, logger) defer logger.Log("done processing commands") switch verb { case "upload": diff --git a/modules/markup/html.go b/modules/markup/html.go index a21c470711..8dafb742d7 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -51,7 +51,7 @@ var globalVars = sync.OnceValue(func() *globalVarsType { v := &globalVarsType{} // NOTE: All below regex matching do not perform any extra validation. // Thus a link is produced even if the linked entity does not exist. - // While fast, this is also incorrect and lead to false positives. + // While fast, this is also incorrect and leads to false positives. // TODO: fix invalid linking issue (update: stale TODO, what issues? maybe no TODO anymore) // valid chars in encoded path and parameter: [-+~_%.a-zA-Z0-9/] diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go index 9c7259dc8c..25a7e1532d 100644 --- a/modules/markup/sanitizer_default.go +++ b/modules/markup/sanitizer_default.go @@ -54,7 +54,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { // Allow 'color' and 'background-color' properties for the style attribute on text elements. policy.AllowStyles("color", "background-color").OnElements("div", "span", "p", "tr", "th", "td") - policy.AllowAttrs("src", "autoplay", "controls").OnElements("video") + policy.AllowAttrs("src", "autoplay", "controls", "muted", "loop", "playsinline").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. diff --git a/modules/packages/arch/metadata.go b/modules/packages/arch/metadata.go index 5960834846..538502921b 100644 --- a/modules/packages/arch/metadata.go +++ b/modules/packages/arch/metadata.go @@ -13,6 +13,7 @@ import ( "strconv" "strings" + "gitea.dev/modules/packages" "gitea.dev/modules/util" "gitea.dev/modules/validation" @@ -46,6 +47,11 @@ var ( namePattern = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`) // (epoch:pkgver-pkgrel) versionPattern = regexp.MustCompile(`\A(?:\d:)?[\w.+~]+(?:-[-\w.+~]+)?\z`) + + // caps on the accumulated package file list (vars so tests can lower them); far above + // any legitimate package, but low enough to stop metadata amplification + maxFileEntries = 100000 + maxFileNameBytes = 16 * 1024 * 1024 ) type Package struct { @@ -124,7 +130,7 @@ func ParsePackage(r io.Reader) (*Package, error) { } var p *Package - files := make([]string, 0, 10) + files := packages.NewBoundedFileList(maxFileEntries, maxFileNameBytes) tr := tar.NewReader(inner) for { @@ -147,7 +153,9 @@ func ParsePackage(r io.Reader) (*Package, error) { return nil, err } } else if !strings.HasPrefix(filename, ".") { - files = append(files, hd.Name) + if err := files.Add(hd.Name); err != nil { + return nil, err + } } } @@ -155,7 +163,7 @@ func ParsePackage(r io.Reader) (*Package, error) { return nil, ErrMissingPKGINFOFile } - p.FileMetadata.Files = files + p.FileMetadata.Files = files.Files() p.FileCompressionExtension = compressionType return p, nil diff --git a/modules/packages/arch/metadata_test.go b/modules/packages/arch/metadata_test.go index 5bdf63a3ee..3e6897d406 100644 --- a/modules/packages/arch/metadata_test.go +++ b/modules/packages/arch/metadata_test.go @@ -10,6 +10,9 @@ import ( "io" "testing" + "gitea.dev/modules/test" + "gitea.dev/modules/util" + "github.com/klauspost/compress/zstd" "github.com/stretchr/testify/assert" "github.com/ulikunitz/xz" @@ -167,3 +170,25 @@ func TestParsePackageInfo(t *testing.T) { assert.ElementsMatch(t, []string{"usr/bin/paket1"}, p.FileMetadata.Backup) }) } + +// TestParsePackageTooManyFiles ensures the accumulated file list is bounded to prevent +// metadata amplification from a package with a huge number of (tiny) file entries. +func TestParsePackageTooManyFiles(t *testing.T) { + defer test.MockVariableValue(&maxFileEntries, 3)() + buf := test.WriteTarCompression(func(w io.Writer) io.WriteCloser { return gzip.NewWriter(w) }, map[string]string{ + "file1": "content1", + ".PKGINFO": string(createPKGINFOContent(packageName, packageVersion)), + }) + _, err := ParsePackage(buf) + assert.NoError(t, err) + + buf = test.WriteTarCompression(func(w io.Writer) io.WriteCloser { return gzip.NewWriter(w) }, map[string]string{ + "file1": "content1", + "file2": "content2", + "file3": "content3", + "file4": "content4", + ".PKGINFO": string(createPKGINFOContent(packageName, packageVersion)), + }) + _, err = ParsePackage(buf) + assert.ErrorIs(t, err, util.ErrInvalidArgument) +} diff --git a/modules/packages/debian/metadata.go b/modules/packages/debian/metadata.go index 8d8b03147f..6a79595658 100644 --- a/modules/packages/debian/metadata.go +++ b/modules/packages/debian/metadata.go @@ -11,6 +11,7 @@ import ( "net/mail" "regexp" "strings" + "sync" "gitea.dev/modules/util" "gitea.dev/modules/validation" @@ -36,18 +37,36 @@ const ( controlTar = "control.tar" ) -var ( - ErrMissingControlFile = util.NewInvalidArgumentErrorf("control file is missing") - ErrUnsupportedCompression = util.NewInvalidArgumentErrorf("unsupported compression algorithm") - ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid") - ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") - ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid") +var GlobalVars = sync.OnceValue(func() (ret struct { + ErrMissingControlFile error + ErrUnsupportedCompression error + ErrInvalidName error + ErrInvalidVersion error + ErrInvalidArchitecture error + + namePattern *regexp.Regexp + versionPattern *regexp.Regexp + symbolPattern *regexp.Regexp +}, +) { + ret.ErrMissingControlFile = util.NewInvalidArgumentErrorf("control file is missing") + ret.ErrUnsupportedCompression = util.NewInvalidArgumentErrorf("unsupported compression algorithm") + ret.ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid") + ret.ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") + ret.ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid") // https://www.debian.org/doc/debian-policy/ch-controlfields.html#source - namePattern = regexp.MustCompile(`\A[a-z0-9][a-z0-9+-.]+\z`) + ret.namePattern = regexp.MustCompile(`\A[a-z0-9][a-z0-9+-.]+\z`) // https://www.debian.org/doc/debian-policy/ch-controlfields.html#version - versionPattern = regexp.MustCompile(`\A(?:(0|[1-9][0-9]*):)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`) -) + ret.versionPattern = regexp.MustCompile(`\A(?:(0|[1-9][0-9]*):)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`) + + // distribution and component are taken from the request path and written + // verbatim into the generated line-based Release and Packages indices (and + // into the pool// paths referenced from them), so + // they must be restricted to a character set that cannot break that format. + ret.symbolPattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9.~+_-]*\z`) + return ret +}) type Package struct { Name string @@ -64,6 +83,10 @@ type Metadata struct { Dependencies []string `json:"dependencies,omitempty"` } +func IsValidDistributionOrComponent(s string) bool { + return GlobalVars().symbolPattern.MatchString(s) +} + // ParsePackage parses the Debian package file // https://manpages.debian.org/bullseye/dpkg-dev/deb.5.en.html func ParsePackage(r io.Reader) (*Package, error) { @@ -109,10 +132,13 @@ func ParsePackage(r io.Reader) (*Package, error) { inner = zr default: - return nil, ErrUnsupportedCompression + return nil, GlobalVars().ErrUnsupportedCompression } - tr := tar.NewReader(inner) + // bound the decompressed control archive: it holds only the small control file + // and maintainer scripts, so a much larger stream is a decompression bomb + const maxControlTarSize = 32 * 1024 * 1024 + tr := tar.NewReader(io.LimitReader(inner, maxControlTarSize)) for { hd, err := tr.Next() if err == io.EOF { @@ -133,7 +159,7 @@ func ParsePackage(r io.Reader) (*Package, error) { } } - return nil, ErrMissingControlFile + return nil, GlobalVars().ErrMissingControlFile } // ParseControlFile parses a Debian control file to retrieve the metadata @@ -145,6 +171,7 @@ func ParseControlFile(r io.Reader) (*Package, error) { key := "" var depends strings.Builder var control strings.Builder + var description strings.Builder // https://www.debian.org/doc/debian-policy/ch-controlfields.html#syntax-of-control-files s := bufio.NewScanner(r) @@ -166,10 +193,13 @@ func ParseControlFile(r io.Reader) (*Package, error) { control.WriteString(line) control.WriteByte('\n') + // a leading space or tab marks a folded continuation line that belongs to the previous field + // (identified by key), not a new "Key: value" pair; only the multi-line fields append here. + // Continuation lines may themselves contain a colon, so they must not be re-split on ":". if line[0] == ' ' || line[0] == '\t' { switch key { case "Description": - p.Metadata.Description += line + description.WriteString(line) case "Depends": depends.WriteString(trimmed) } @@ -196,7 +226,8 @@ func ParseControlFile(r io.Reader) (*Package, error) { p.Metadata.Maintainer = a.Name } case "Description": - p.Metadata.Description = value + description.Reset() + description.WriteString(value) case "Depends": depends.WriteString(value) case "Homepage": @@ -210,16 +241,18 @@ func ParseControlFile(r io.Reader) (*Package, error) { return nil, err } - if !namePattern.MatchString(p.Name) { - return nil, ErrInvalidName + if !GlobalVars().namePattern.MatchString(p.Name) { + return nil, GlobalVars().ErrInvalidName } - if !versionPattern.MatchString(p.Version) { - return nil, ErrInvalidVersion + if !GlobalVars().versionPattern.MatchString(p.Version) { + return nil, GlobalVars().ErrInvalidVersion } if p.Architecture == "" { - return nil, ErrInvalidArchitecture + return nil, GlobalVars().ErrInvalidArchitecture } + p.Metadata.Description = description.String() + dependencies := strings.Split(depends.String(), ",") for i := range dependencies { dependencies[i] = strings.TrimSpace(dependencies[i]) diff --git a/modules/packages/debian/metadata_test.go b/modules/packages/debian/metadata_test.go index 6ff10a7f21..20d8c5f509 100644 --- a/modules/packages/debian/metadata_test.go +++ b/modules/packages/debian/metadata_test.go @@ -49,7 +49,7 @@ func TestParsePackage(t *testing.T) { p, err := ParsePackage(data) assert.Nil(t, p) - assert.ErrorIs(t, err, ErrMissingControlFile) + assert.ErrorIs(t, err, GlobalVars().ErrMissingControlFile) }) t.Run("Compression", func(t *testing.T) { @@ -58,7 +58,7 @@ func TestParsePackage(t *testing.T) { p, err := ParsePackage(data) assert.Nil(t, p) - assert.ErrorIs(t, err, ErrUnsupportedCompression) + assert.ErrorIs(t, err, GlobalVars().ErrUnsupportedCompression) }) var buf bytes.Buffer @@ -141,7 +141,7 @@ func TestParseControlFile(t *testing.T) { for _, name := range []string{"", "-cd"} { p, err := ParseControlFile(buildContent(name, packageVersion, packageArchitecture)) assert.Nil(t, p) - assert.ErrorIs(t, err, ErrInvalidName) + assert.ErrorIs(t, err, GlobalVars().ErrInvalidName) } }) @@ -149,14 +149,14 @@ func TestParseControlFile(t *testing.T) { for _, version := range []string{"", "1-", ":1.0", "1_0"} { p, err := ParseControlFile(buildContent(packageName, version, packageArchitecture)) assert.Nil(t, p) - assert.ErrorIs(t, err, ErrInvalidVersion) + assert.ErrorIs(t, err, GlobalVars().ErrInvalidVersion) } }) t.Run("InvalidArchitecture", func(t *testing.T) { p, err := ParseControlFile(buildContent(packageName, packageVersion, "")) assert.Nil(t, p) - assert.ErrorIs(t, err, ErrInvalidArchitecture) + assert.ErrorIs(t, err, GlobalVars().ErrInvalidArchitecture) }) t.Run("Valid", func(t *testing.T) { @@ -200,3 +200,47 @@ func TestParseControlFile(t *testing.T) { assert.NotContains(t, p.Control, "evil.deb") }) } + +func TestValidateDistributionOrComponent(t *testing.T) { + bad := []string{ + "", + ".", + "..", + "-stable", + ".hidden", + "a/b", + "a b", + "bookworm\nSigned-By: evil", + "main\nFilename: pool/x", + "a\tb", + } + for _, name := range bad { + assert.False(t, IsValidDistributionOrComponent(name), "bad=%q", name) + } + + good := []string{ + "stable", + "bookworm", + "bookworm-backports", + "stable-updates", + "main", + "non-free-firmware", + "a", + "1", + } + for _, name := range good { + assert.True(t, IsValidDistributionOrComponent(name), "good=%q", name) + } +} + +// TestParseControlFileMultilineDescription verifies a multi-line Description is assembled in order +// (the parser accumulates it in a strings.Builder); it guards the assembled value, not its timing. +func TestParseControlFileMultilineDescription(t *testing.T) { + var buf bytes.Buffer + buf.WriteString("Package: testpkg\nVersion: 1.0\nArchitecture: amd64\nDescription: short summary\n more details\n even more\n") + + p, err := ParseControlFile(&buf) + assert.NoError(t, err) + assert.NotNil(t, p) + assert.Equal(t, "short summary more details even more", p.Metadata.Description) +} diff --git a/modules/packages/filelist.go b/modules/packages/filelist.go new file mode 100644 index 0000000000..2e3676fbe4 --- /dev/null +++ b/modules/packages/filelist.go @@ -0,0 +1,37 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package packages + +import "gitea.dev/modules/util" + +// BoundedFileList accumulates file names from a package archive while enforcing caps on the number of +// entries and their total name length, returning an error once either cap would be exceeded. +type BoundedFileList struct { + files []string + nameBytes int + maxFiles int + maxBytes int +} + +// NewBoundedFileList creates a BoundedFileList with the given caps; a non-positive cap falls back to the +// corresponding default. +func NewBoundedFileList(maxFiles, maxNameBytes int) *BoundedFileList { + return &BoundedFileList{maxFiles: maxFiles, maxBytes: maxNameBytes} +} + +// Add appends name, returning util.ErrInvalidArgument once the entry count or accumulated byte length +// would exceed the configured cap. +func (b *BoundedFileList) Add(name string) error { + if len(b.files) >= b.maxFiles || b.nameBytes+len(name) > b.maxBytes { + return util.NewInvalidArgumentErrorf("package contains too many file entries") + } + b.nameBytes += len(name) + b.files = append(b.files, name) + return nil +} + +// Files returns the accumulated file names. +func (b *BoundedFileList) Files() []string { + return b.files +} diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index 0ed66fbcb0..e419898851 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -103,7 +103,7 @@ type PackageMetadataVersion struct { DevDependencies map[string]string `json:"devDependencies,omitempty"` PeerDependencies map[string]string `json:"peerDependencies,omitempty"` PeerDependenciesMeta map[string]any `json:"peerDependenciesMeta,omitempty"` - Bin map[string]string `json:"bin,omitempty"` + Bin Bin `json:"bin,omitempty"` OptionalDependencies map[string]string `json:"optionalDependencies,omitempty"` Readme string `json:"readme,omitempty"` Dist PackageDistribution `json:"dist"` @@ -188,6 +188,49 @@ type Repository struct { Directory string `json:"directory,omitempty"` } +// UnmarshalJSON is needed because the repository field can be a string or an object. +func (r *Repository) UnmarshalJSON(data []byte) error { + switch data[0] { + case '"': + var value string + if err := json.Unmarshal(data, &value); err != nil { + return err + } + r.URL = value + case '{': + type repositoryAlias Repository // avoid recursion into this method + var value repositoryAlias + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *r = Repository(value) + } + return nil +} + +// Bin maps command names to executable files. npm also allows a single string, +// in which case the command is named after the package (resolved in ParsePackage). +type Bin map[string]string + +// UnmarshalJSON is needed because the bin field can be a string or an object. +func (b *Bin) UnmarshalJSON(data []byte) error { + switch data[0] { + case '"': + var value string + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *b = Bin{"": value} + case '{': + var value map[string]string + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *b = value + } + return nil +} + // PackageAttachment https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package type PackageAttachment struct { ContentType string `json:"content_type"` @@ -229,6 +272,11 @@ func ParsePackage(r io.Reader) (*Package, error) { meta.Homepage = "" } + // A string "bin" means a single executable named after the package. + if cmd, ok := meta.Bin[""]; ok && len(meta.Bin) == 1 { + meta.Bin = Bin{name: cmd} + } + p := &Package{ Name: meta.Name, Version: v.String(), diff --git a/modules/packages/npm/creator_test.go b/modules/packages/npm/creator_test.go index a9e42f4422..02e17c2c46 100644 --- a/modules/packages/npm/creator_test.go +++ b/modules/packages/npm/creator_test.go @@ -326,4 +326,31 @@ func TestParsePackage(t *testing.T) { require.NoError(t, err) require.Equal(t, "MIT", string(p.Metadata.License)) }) + + t.Run("ValidRepositoryAndBinAsString", func(t *testing.T) { + // npm allows "repository" and "bin" to be plain strings, not only objects. + packageJSON := `{ + "versions": { + "0.1.1": { + "name": "dev-null", + "version": "0.1.1", + "bin": "./cli.js", + "repository": "https://gitea.io/gitea/test.git", + "dist": { + "integrity": "sha256-" + } + } + }, + "_attachments": { + "foo": { + "data": "AAAA" + } + } +}` + p, err := ParsePackage(strings.NewReader(packageJSON)) + require.NoError(t, err) + require.Equal(t, "https://gitea.io/gitea/test.git", p.Metadata.Repository.URL) + // a string bin is named after the package + require.Equal(t, "./cli.js", p.Metadata.Bin["dev-null"]) + }) } diff --git a/modules/packages/terraform/lock.go b/modules/packages/terraform/lock.go index ed9edad8e2..1bce4b109e 100644 --- a/modules/packages/terraform/lock.go +++ b/modules/packages/terraform/lock.go @@ -79,13 +79,12 @@ func RemoveLock(ctx context.Context, packageID int64) error { } 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) + pp, ok, err := db.Get[packages_model.PackageProperty](ctx, builder.Eq{"ref_type": packages_model.PropertyTypePackage, "ref_id": refID, "`name`": LockFile}) 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}) + n, err := db.GetEngine(ctx).ID(pp.ID).And(cond).Cols("value").Update(&packages_model.PackageProperty{Value: value}) if err != nil { return err } diff --git a/modules/pprof/pprof.go b/modules/pprof/pprof.go index 138d33305f..c00bfb8f16 100644 --- a/modules/pprof/pprof.go +++ b/modules/pprof/pprof.go @@ -6,15 +6,15 @@ package pprof import ( "fmt" "os" + "path/filepath" "runtime" "runtime/pprof" "gitea.dev/modules/log" ) -// DumpMemProfileForUsername dumps a memory profile at pprofDataPath as memprofile__ -func DumpMemProfileForUsername(pprofDataPath, username string) error { - f, err := os.CreateTemp(pprofDataPath, fmt.Sprintf("memprofile_%s_", username)) +func dumpMemProfileForUsername(pprofDataPath, subName string) error { + f, err := os.CreateTemp(pprofDataPath, fmt.Sprintf("pprof_mem_%s_", filepath.Clean(subName))) if err != nil { return err } @@ -23,23 +23,30 @@ func DumpMemProfileForUsername(pprofDataPath, username string) error { return pprof.WriteHeapProfile(f) } -// DumpCPUProfileForUsername dumps a CPU profile at pprofDataPath as cpuprofile__ -// the stop function it returns stops, writes and closes the CPU profile file -func DumpCPUProfileForUsername(pprofDataPath, username string) (func(), error) { - f, err := os.CreateTemp(pprofDataPath, fmt.Sprintf("cpuprofile_%s_", username)) +func DumpPprofForUsername(pprofDataPath, subName string) (func(), error) { + if err := os.MkdirAll(pprofDataPath, os.ModePerm); err != nil { + return nil, fmt.Errorf(`os.MkdirAll(pprofDataPath) failed: %v`, err) + } + + f, err := os.CreateTemp(pprofDataPath, fmt.Sprintf("pprof_cpu_%s_", filepath.Clean(subName))) if err != nil { return nil, err } err = pprof.StartCPUProfile(f) if err != nil { - log.Fatal("StartCPUProfile: %v", err) + _ = f.Close() + return nil, fmt.Errorf("StartCPUProfile: %w", err) } return func() { pprof.StopCPUProfile() err = f.Close() if err != nil { - log.Fatal("StopCPUProfile Close: %v", err) + log.Error("StopCPUProfile Close: %v", err) + } + err = dumpMemProfileForUsername(pprofDataPath, subName) + if err != nil { + log.Error("DumpMemProfile: %v", err) } }, nil } diff --git a/modules/private/internal.go b/modules/private/internal.go index 3d6b4ee4bc..fe74378eb3 100644 --- a/modules/private/internal.go +++ b/modules/private/internal.go @@ -8,6 +8,7 @@ import ( "crypto/tls" "net" "net/http" + "net/url" "os" "strings" "sync" @@ -53,12 +54,37 @@ func dialContextInternalAPI(ctx context.Context, network, address string) (conn return conn, nil } +// internalAPIConnectionIsLocal reports whether the internal API transport connects to a local target, +// where the self-signed local certificate cannot be verified so skipping verification is safe. It mirrors +// what dialContextInternalAPI actually dials: a unix socket whenever Protocol is HTTPUnix (always local, +// whatever LOCAL_ROOT_URL says), otherwise the LOCAL_ROOT_URL host directly. A non-loopback LOCAL_ROOT_URL +// is a real network hop, so its certificate must be verified, else the internal token can be MITM'd. An +// unparseable LOCAL_ROOT_URL is a hard misconfiguration and fails closed (verify). +func internalAPIConnectionIsLocal(protocol setting.Scheme, localURL string) bool { + if protocol == setting.HTTPUnix { + return true + } + u, err := url.Parse(localURL) + if err != nil { + return false + } + host := u.Hostname() + if host == "localhost" { + return true + } + ip := net.ParseIP(host) + return ip != nil && ip.IsLoopback() +} + var internalAPITransport = sync.OnceValue(func() http.RoundTripper { return &http.Transport{ DialContext: dialContextInternalAPI, TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - ServerName: setting.Domain, + // Skip verification only for a local target (unix socket, or a loopback LOCAL_ROOT_URL), where the + // self-signed local cert can't be verified anyway; a non-loopback LOCAL_ROOT_URL is a real network + // hop and must be verified so the internal token can't be MITM'd. When verifying, Go's default + // ServerName (the dialed LOCAL_ROOT_URL host) is already correct, so it is not overridden. + InsecureSkipVerify: internalAPIConnectionIsLocal(setting.Protocol, setting.LocalURL), }, } }) diff --git a/modules/private/internal_test.go b/modules/private/internal_test.go new file mode 100644 index 0000000000..35713d1b0f --- /dev/null +++ b/modules/private/internal_test.go @@ -0,0 +1,37 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "testing" + + "gitea.dev/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func TestInternalAPIConnectionIsLocal(t *testing.T) { + cases := []struct { + name string + protocol setting.Scheme + localURL string + want bool + }{ + // HTTPUnix always dials the unix socket (a local target), whatever LOCAL_ROOT_URL says + {"unix socket", setting.HTTPUnix, "https://gitea.example.com/", true}, + {"localhost", setting.HTTP, "http://localhost:3000/", true}, + {"loopback ipv4", setting.HTTPS, "https://127.0.0.1:3000/", true}, + {"loopback ipv6", setting.HTTPS, "https://[::1]:3000/", true}, + // a non-loopback LOCAL_ROOT_URL is a real network hop and must be verified + {"remote host", setting.HTTPS, "https://gitea.internal:443/", false}, + {"remote ip", setting.HTTPS, "https://10.0.0.5:3000/", false}, + // an unparseable LOCAL_ROOT_URL is a hard misconfiguration; fail closed to verification + {"invalid url", setting.HTTPS, "://bad", false}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + assert.Equal(t, c.want, internalAPIConnectionIsLocal(c.protocol, c.localURL)) + }) + } +} diff --git a/modules/private/serv.go b/modules/private/serv.go index cde255f36a..20768d1019 100644 --- a/modules/private/serv.go +++ b/modules/private/serv.go @@ -43,6 +43,8 @@ type ServCommandResults struct { OwnerName string RepoName string RepoID int64 + + RepoStoragePath string } // ServCommand preps for a serv call diff --git a/modules/references/references.go b/modules/references/references.go index 67a77f171a..b14cc5f435 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -24,7 +24,7 @@ var ( // NOTE: All below regex matching do not perform any extra validation. // Thus a link is produced even if the linked entity does not exist. - // While fast, this is also incorrect and lead to false positives. + // While fast, this is also incorrect and leads to false positives. // TODO: fix invalid linking issue // mentionPattern matches all mentions in the form of "@user" or "@org/team" diff --git a/modules/repository/branch.go b/modules/repository/branch.go index 48c5a65da8..21fbd3a9af 100644 --- a/modules/repository/branch.go +++ b/modules/repository/branch.go @@ -33,7 +33,7 @@ func SyncRepoBranches(ctx context.Context, repoID, doerID int64) (int64, error) log.Debug("SyncRepoBranches: in Repo[%d:%s]", repo.ID, repo.FullName()) - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + gitRepo, err := gitrepo.OpenRepository(repo) if err != nil { log.Error("OpenRepository[%s]: %w", repo.FullName(), err) return 0, err @@ -45,7 +45,7 @@ func SyncRepoBranches(ctx context.Context, repoID, doerID int64) (int64, error) } func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doerID int64) (int64, []*SyncResult, error) { - objFmt, err := gitRepo.GetObjectFormat() + objFmt, err := gitRepo.GetObjectFormat(ctx) if err != nil { return 0, nil, fmt.Errorf("GetObjectFormat: %w", err) } @@ -58,7 +58,7 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, allBranches := container.Set[string]{} { - branches, _, err := gitRepo.GetBranchNames(0, 0) + branches, _, err := gitRepo.GetBranchNames(ctx, 0, 0) if err != nil { return 0, nil, err } @@ -88,7 +88,7 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, var syncResults []*SyncResult for branch := range allBranches { dbb := dbBranches[branch] - commit, err := gitRepo.GetBranchCommit(branch) + commit, err := gitRepo.GetBranchCommit(ctx, branch) if err != nil { return 0, nil, err } diff --git a/modules/repository/repo.go b/modules/repository/repo.go index 35dd74cd6d..d24ed449ef 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -47,7 +47,7 @@ func SyncRepoTags(ctx context.Context, repoID int64) error { return err } - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + gitRepo, err := gitrepo.OpenRepository(repo) if err != nil { return err } @@ -181,7 +181,7 @@ func (shortRelease) TableName() string { // repositories like https://github.com/vim/vim (with over 13000 tags). func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) ([]*SyncResult, error) { log.Debug("SyncReleasesWithTags: in Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name) - tags, _, err := gitRepo.GetTagInfos(0, 0) + tags, _, err := gitRepo.GetTagInfos(ctx, 0, 0) if err != nil { return nil, fmt.Errorf("unable to GetTagInfos in pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) } diff --git a/modules/setting/actions.go b/modules/setting/actions.go index d8e229267c..6043d53ae5 100644 --- a/modules/setting/actions.go +++ b/modules/setting/actions.go @@ -14,6 +14,8 @@ import ( const defaultMaxRerunAttempts = 50 +const defaultMaxConcurrentTaskPicks = 16 + // Actions settings var ( Actions = struct { @@ -29,13 +31,20 @@ var ( AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"` SkipWorkflowStrings []string `ini:"SKIP_WORKFLOW_STRINGS"` WorkflowDirs []string `ini:"WORKFLOW_DIRS"` + ScopedWorkflowDirs []string `ini:"SCOPED_WORKFLOW_DIRS"` MaxRerunAttempts int64 `ini:"MAX_RERUN_ATTEMPTS"` + // MaxConcurrentTaskPicks bounds how many runners may run the task-assignment + // transaction at once per Gitea instance, to avoid a thundering herd when many + // runners poll together. It is a per-process limit, not a cluster-wide one. + MaxConcurrentTaskPicks int `ini:"MAX_CONCURRENT_TASK_PICKS"` }{ - Enabled: true, - DefaultActionsURL: defaultActionsURLGitHub, - SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"}, - WorkflowDirs: []string{".gitea/workflows", ".github/workflows"}, - MaxRerunAttempts: defaultMaxRerunAttempts, + Enabled: true, + DefaultActionsURL: defaultActionsURLGitHub, + SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"}, + WorkflowDirs: []string{".gitea/workflows", ".github/workflows"}, + ScopedWorkflowDirs: []string{".gitea/scoped_workflows"}, + MaxRerunAttempts: defaultMaxRerunAttempts, + MaxConcurrentTaskPicks: defaultMaxConcurrentTaskPicks, } ) @@ -126,24 +135,47 @@ func loadActionsFrom(rootCfg ConfigProvider) error { Actions.MaxRerunAttempts = defaultMaxRerunAttempts } + if Actions.MaxConcurrentTaskPicks <= 0 { + Actions.MaxConcurrentTaskPicks = defaultMaxConcurrentTaskPicks + } + if !Actions.LogCompression.IsValid() { return fmt.Errorf("invalid [actions] LOG_COMPRESSION: %q", Actions.LogCompression) } - workflowDirs := make([]string, 0, len(Actions.WorkflowDirs)) - for _, dir := range Actions.WorkflowDirs { + workflowDirs := normalizeWorkflowDirs(Actions.WorkflowDirs) + if len(workflowDirs) == 0 { + return errors.New("[actions] WORKFLOW_DIRS must contain at least one entry") + } + Actions.WorkflowDirs = workflowDirs + + // SCOPED_WORKFLOW_DIRS may be empty (feature disabled), but it must not overlap with WORKFLOW_DIRS: + // a scoped dir nested in (or equal to) a workflow dir would make the same file run both repo-level and scope-level. + Actions.ScopedWorkflowDirs = normalizeWorkflowDirs(Actions.ScopedWorkflowDirs) + for _, scopedDir := range Actions.ScopedWorkflowDirs { + for _, workflowDir := range Actions.WorkflowDirs { + if scopedDir == workflowDir || + strings.HasPrefix(scopedDir, workflowDir+"/") || + strings.HasPrefix(workflowDir, scopedDir+"/") { + return fmt.Errorf("[actions] SCOPED_WORKFLOW_DIRS entry %q overlaps with WORKFLOW_DIRS entry %q", scopedDir, workflowDir) + } + } + } + + return nil +} + +// normalizeWorkflowDirs trims, normalizes separators and drops empty/trailing-slash entries. +func normalizeWorkflowDirs(dirs []string) []string { + normalized := make([]string, 0, len(dirs)) + for _, dir := range dirs { dir = strings.TrimSpace(dir) if dir == "" { continue } dir = strings.ReplaceAll(dir, `\`, `/`) dir = strings.TrimRight(dir, "/") - workflowDirs = append(workflowDirs, dir) + normalized = append(normalized, dir) } - if len(workflowDirs) == 0 { - return errors.New("[actions] WORKFLOW_DIRS must contain at least one entry") - } - Actions.WorkflowDirs = workflowDirs - - return nil + return normalized } diff --git a/modules/setting/actions_test.go b/modules/setting/actions_test.go index 5c7ab268c1..5b2e5ac3d9 100644 --- a/modules/setting/actions_test.go +++ b/modules/setting/actions_test.go @@ -5,8 +5,11 @@ package setting import ( "path/filepath" + "slices" "testing" + "gitea.dev/modules/test" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -238,3 +241,71 @@ DEFAULT_ACTIONS_URL = gitea }) } } + +func Test_ScopedWorkflowDirs(t *testing.T) { + defer test.MockVariableValue(&Actions)() + + defaultWorkflowDirs := []string{".gitea/workflows", ".github/workflows"} + defaultScopedDirs := []string{".gitea/scoped_workflows"} + + tests := []struct { + name string + iniStr string + wantScoped []string + wantErr bool + }{ + { + name: "default", + iniStr: `[actions]`, + wantScoped: defaultScopedDirs, + }, + { + name: "custom dir", + iniStr: "[actions]\nSCOPED_WORKFLOW_DIRS = .gitea/my-scoped", + wantScoped: []string{".gitea/my-scoped"}, + }, + { + name: "empty disables the feature", + iniStr: "[actions]\nSCOPED_WORKFLOW_DIRS = , ,", + wantScoped: []string{}, + }, + { + name: "overlap equal with workflow dir", + iniStr: "[actions]\nWORKFLOW_DIRS = .gitea/workflows\nSCOPED_WORKFLOW_DIRS = .gitea/workflows", + wantErr: true, + }, + { + name: "scoped dir nested under workflow dir", + iniStr: "[actions]\nWORKFLOW_DIRS = .gitea/workflows\nSCOPED_WORKFLOW_DIRS = .gitea/workflows/scoped", + wantErr: true, + }, + { + name: "workflow dir nested under scoped dir", + iniStr: "[actions]\nWORKFLOW_DIRS = .gitea/workflows/ci\nSCOPED_WORKFLOW_DIRS = .gitea/workflows", + wantErr: true, + }, + { + name: "no overlap", + iniStr: "[actions]\nSCOPED_WORKFLOW_DIRS = .gitea/scoped", + wantScoped: []string{".gitea/scoped"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // reset to defaults so MapTo starts clean (absent keys keep the defaults) + Actions.WorkflowDirs = slices.Clone(defaultWorkflowDirs) + Actions.ScopedWorkflowDirs = slices.Clone(defaultScopedDirs) + + cfg, err := NewConfigProviderFromData(tt.iniStr) + require.NoError(t, err) + err = loadActionsFrom(cfg) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantScoped, Actions.ScopedWorkflowDirs) + }) + } +} diff --git a/modules/setting/repository.go b/modules/setting/repository.go index d1000c5280..8f3fa049a3 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -22,6 +22,7 @@ const ( const ( RepoPRTitleSourceFirstCommit = "first-commit" RepoPRTitleSourceAuto = "auto" + RepoPRTitleSourceBranchName = "branch-name" ) // ItemsPerPage maximum items per page in forks, watchers and stars of a repo diff --git a/modules/setting/security.go b/modules/setting/security.go index a72bd90214..e8f5e1f2ab 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -20,9 +20,11 @@ var Security = struct { XContentTypeOptions string ContentSecurityPolicyGeneral string // it only supports empty (default policy) or "unset", maybe it can support more in the future + AllowedHostList string }{ XFrameOptions: "SAMEORIGIN", XContentTypeOptions: "nosniff", + AllowedHostList: "external", } var ( diff --git a/modules/setting/security_test.go b/modules/setting/security_test.go index 70fdc77d12..d830a51794 100644 --- a/modules/setting/security_test.go +++ b/modules/setting/security_test.go @@ -10,13 +10,20 @@ import ( ) func TestLoadSecurityFrom(t *testing.T) { + assert.Equal(t, "SAMEORIGIN", Security.XFrameOptions) + assert.Equal(t, "nosniff", Security.XContentTypeOptions) + assert.Equal(t, "external", Security.AllowedHostList) + cfg, err := NewConfigProviderFromData(`[security] X_FRAME_OPTIONS = DENY X_CONTENT_TYPE_OPTIONS = unset -CONTENT_SECURITY_POLICY_GENERAL = "script-src *; foo"`) +ALLOWED_HOST_LIST = foo +CONTENT_SECURITY_POLICY_GENERAL = "script-src *; foo" +`) assert.NoError(t, err) loadSecurityFrom(cfg) assert.Equal(t, "DENY", Security.XFrameOptions) assert.Equal(t, "unset", Security.XContentTypeOptions) + assert.Equal(t, "foo", Security.AllowedHostList) assert.Equal(t, `"script-src *`, Security.ContentSecurityPolicyGeneral) // holy shit ini package bug } diff --git a/modules/setting/service.go b/modules/setting/service.go index b578ce4d2e..ad5c73556e 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -25,12 +25,9 @@ const ( // Service settings var Service = struct { - DefaultUserVisibility string - DefaultUserVisibilityMode structs.VisibleType - AllowedUserVisibilityModes []string - AllowedUserVisibilityModesSlice AllowedVisibility `ini:"-"` - DefaultOrgVisibility string - DefaultOrgVisibilityMode structs.VisibleType + DefaultUserVisibilityMode structs.VisibleType `ini:"-"` + AllowedUserVisibilityModesSlice AllowedVisibility `ini:"-"` + DefaultOrgVisibilityMode structs.VisibleType `ini:"-"` ActiveCodeLives int ResetPwdCodeLives int RegisterEmailConfirm bool @@ -217,36 +214,35 @@ func loadServiceFrom(rootCfg ConfigProvider) { Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true) Service.AutoWatchNewRepos = sec.Key("AUTO_WATCH_NEW_REPOS").MustBool(true) Service.AutoWatchOnChanges = sec.Key("AUTO_WATCH_ON_CHANGES").MustBool(false) - modes := sec.Key("ALLOWED_USER_VISIBILITY_MODES").Strings(",") - if len(modes) != 0 { - Service.AllowedUserVisibilityModes = []string{} - Service.AllowedUserVisibilityModesSlice = []bool{false, false, false} - for _, sMode := range modes { - if tp, ok := structs.VisibilityModes[sMode]; ok { // remove unsupported modes - Service.AllowedUserVisibilityModes = append(Service.AllowedUserVisibilityModes, sMode) - Service.AllowedUserVisibilityModesSlice[tp] = true - } else { - log.Warn("ALLOWED_USER_VISIBILITY_MODES %s is unsupported", sMode) - } + + Service.AllowedUserVisibilityModesSlice = AllowedVisibility{false, false, false} + var allowedUserVisibilityModes []structs.VisibilityString + for _, allowedMode := range sec.Key("ALLOWED_USER_VISIBILITY_MODES").Strings(",") { + if tp, ok := structs.VisibilityModes[structs.VisibilityString(allowedMode)]; ok { // remove unsupported modes + allowedUserVisibilityModes = append(allowedUserVisibilityModes, structs.VisibilityString(allowedMode)) + Service.AllowedUserVisibilityModesSlice[tp] = true + } else { + log.Warn("ALLOWED_USER_VISIBILITY_MODES %s is unsupported", allowedMode) } } - - if len(Service.AllowedUserVisibilityModes) == 0 { - Service.AllowedUserVisibilityModes = []string{"public", "limited", "private"} - Service.AllowedUserVisibilityModesSlice = []bool{true, true, true} + if len(allowedUserVisibilityModes) == 0 { + allowedUserVisibilityModes = []structs.VisibilityString{structs.VisibilityStringPublic, structs.VisibilityStringLimited, structs.VisibilityStringPrivate} + Service.AllowedUserVisibilityModesSlice = AllowedVisibility{true, true, true} } - Service.DefaultUserVisibility = sec.Key("DEFAULT_USER_VISIBILITY").String() - if Service.DefaultUserVisibility == "" { - Service.DefaultUserVisibility = Service.AllowedUserVisibilityModes[0] - } else if !Service.AllowedUserVisibilityModesSlice[structs.VisibilityModes[Service.DefaultUserVisibility]] { - log.Warn("DEFAULT_USER_VISIBILITY %s is wrong or not in ALLOWED_USER_VISIBILITY_MODES, using first allowed", Service.DefaultUserVisibility) - Service.DefaultUserVisibility = Service.AllowedUserVisibilityModes[0] + defaultUserVisibility := structs.VisibilityString(sec.Key("DEFAULT_USER_VISIBILITY").String()) + if defaultUserVisibility == "" { + defaultUserVisibility = allowedUserVisibilityModes[0] + } else if vt, ok := structs.VisibilityModes[defaultUserVisibility]; !ok || !Service.AllowedUserVisibilityModesSlice[vt] { + log.Warn("DEFAULT_USER_VISIBILITY %s is wrong or not in ALLOWED_USER_VISIBILITY_MODES, using first allowed", defaultUserVisibility) + defaultUserVisibility = allowedUserVisibilityModes[0] } - Service.DefaultUserVisibilityMode = structs.VisibilityModes[Service.DefaultUserVisibility] - Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes)) - Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility] + Service.DefaultUserVisibilityMode = structs.VisibilityModes[defaultUserVisibility] + + defaultOrgVisibility := structs.VisibilityString(sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.VisibilityModeKeys())) + Service.DefaultOrgVisibilityMode = structs.VisibilityModes[defaultOrgVisibility] Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool() + Service.UserDeleteWithCommentsMaxTime = sec.Key("USER_DELETE_WITH_COMMENTS_MAX_TIME").MustDuration(0) sec.Key("VALID_SITE_URL_SCHEMES").MustString("http,https") Service.ValidSiteURLSchemes = sec.Key("VALID_SITE_URL_SCHEMES").Strings(",") diff --git a/modules/setting/service_test.go b/modules/setting/service_test.go index 4358f0db67..1527d4f263 100644 --- a/modules/setting/service_test.go +++ b/modules/setting/service_test.go @@ -48,80 +48,83 @@ EMAIL_DOMAIN_BLOCKLIST = d3, *.b func TestLoadServiceVisibilityModes(t *testing.T) { defer test.MockVariableValue(&Service)() - kases := map[string]func(){ + visibleTypeSlice := func(s ...structs.VisibilityString) (ret []structs.VisibleType) { + for _, v := range s { + ret = append(ret, structs.VisibilityModes[v]) + } + return ret + } + testCases := map[string]func(){ ` [service] DEFAULT_USER_VISIBILITY = public ALLOWED_USER_VISIBILITY_MODES = public,limited,private `: func() { - assert.Equal(t, "public", Service.DefaultUserVisibility) assert.Equal(t, structs.VisibleTypePublic, Service.DefaultUserVisibilityMode) - assert.Equal(t, []string{"public", "limited", "private"}, Service.AllowedUserVisibilityModes) + assert.Equal(t, visibleTypeSlice("public", "limited", "private"), Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()) }, ` [service] DEFAULT_USER_VISIBILITY = public `: func() { - assert.Equal(t, "public", Service.DefaultUserVisibility) assert.Equal(t, structs.VisibleTypePublic, Service.DefaultUserVisibilityMode) - assert.Equal(t, []string{"public", "limited", "private"}, Service.AllowedUserVisibilityModes) + assert.Equal(t, visibleTypeSlice("public", "limited", "private"), Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()) }, ` [service] DEFAULT_USER_VISIBILITY = limited `: func() { - assert.Equal(t, "limited", Service.DefaultUserVisibility) assert.Equal(t, structs.VisibleTypeLimited, Service.DefaultUserVisibilityMode) - assert.Equal(t, []string{"public", "limited", "private"}, Service.AllowedUserVisibilityModes) + assert.Equal(t, visibleTypeSlice("public", "limited", "private"), Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()) }, ` [service] ALLOWED_USER_VISIBILITY_MODES = public,limited,private `: func() { - assert.Equal(t, "public", Service.DefaultUserVisibility) assert.Equal(t, structs.VisibleTypePublic, Service.DefaultUserVisibilityMode) - assert.Equal(t, []string{"public", "limited", "private"}, Service.AllowedUserVisibilityModes) + assert.Equal(t, visibleTypeSlice("public", "limited", "private"), Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()) }, ` [service] DEFAULT_USER_VISIBILITY = public ALLOWED_USER_VISIBILITY_MODES = limited,private `: func() { - assert.Equal(t, "limited", Service.DefaultUserVisibility) assert.Equal(t, structs.VisibleTypeLimited, Service.DefaultUserVisibilityMode) - assert.Equal(t, []string{"limited", "private"}, Service.AllowedUserVisibilityModes) + assert.Equal(t, visibleTypeSlice("limited", "private"), Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()) }, ` [service] DEFAULT_USER_VISIBILITY = my_type ALLOWED_USER_VISIBILITY_MODES = limited,private `: func() { - assert.Equal(t, "limited", Service.DefaultUserVisibility) assert.Equal(t, structs.VisibleTypeLimited, Service.DefaultUserVisibilityMode) - assert.Equal(t, []string{"limited", "private"}, Service.AllowedUserVisibilityModes) + assert.Equal(t, visibleTypeSlice("limited", "private"), Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()) + }, + ` +[service] +DEFAULT_USER_VISIBILITY = my_type +`: func() { + assert.Equal(t, structs.VisibleTypePublic, Service.DefaultUserVisibilityMode) + assert.Equal(t, visibleTypeSlice("public", "limited", "private"), Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()) }, ` [service] DEFAULT_USER_VISIBILITY = public ALLOWED_USER_VISIBILITY_MODES = public, limit, privated `: func() { - assert.Equal(t, "public", Service.DefaultUserVisibility) assert.Equal(t, structs.VisibleTypePublic, Service.DefaultUserVisibilityMode) - assert.Equal(t, []string{"public"}, Service.AllowedUserVisibilityModes) + assert.Equal(t, visibleTypeSlice("public"), Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()) }, } - for kase, fun := range kases { - t.Run(kase, func(t *testing.T) { - cfg, err := NewConfigProviderFromData(kase) + for tc, fn := range testCases { + t.Run(tc, func(t *testing.T) { + Service.AllowedUserVisibilityModesSlice = []bool{true, true, true} + Service.DefaultUserVisibilityMode = structs.VisibleTypePublic + cfg, err := NewConfigProviderFromData(tc) assert.NoError(t, err) loadServiceFrom(cfg) - fun() - // reset - Service.AllowedUserVisibilityModesSlice = []bool{true, true, true} - Service.AllowedUserVisibilityModes = []string{} - Service.DefaultUserVisibility = "" - Service.DefaultUserVisibilityMode = structs.VisibleTypePublic + fn() }) } } diff --git a/modules/setting/webhook.go b/modules/setting/webhook.go index 3df5183bb3..18df308479 100644 --- a/modules/setting/webhook.go +++ b/modules/setting/webhook.go @@ -34,7 +34,10 @@ func loadWebhookFrom(rootCfg ConfigProvider) { Webhook.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000) Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5) Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool() - Webhook.AllowedHostList = sec.Key("ALLOWED_HOST_LIST").MustString("") + + deprecatedSetting(rootCfg, "webhook", "ALLOWED_HOST_LIST", "security", "ALLOWED_HOST_LIST", "v28.0.0") + Webhook.AllowedHostList = sec.Key("ALLOWED_HOST_LIST").MustString(Security.AllowedHostList) + Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "matrix", "wechatwork", "packagist"} Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10) Webhook.ProxyURL = sec.Key("PROXY_URL").MustString("") diff --git a/modules/ssh/ssh_test.go b/modules/ssh/ssh_test.go index ad9ac813d4..c82aff13f7 100644 --- a/modules/ssh/ssh_test.go +++ b/modules/ssh/ssh_test.go @@ -73,17 +73,18 @@ func TestInitKeys(t *testing.T) { require.NoError(t, err) assert.Len(t, keyFiles, len(keyTypes)) - metadata := map[string]os.FileInfo{} + // Record file contents so regeneration can be detected + content := map[string][]byte{} for _, keyType := range keyTypes { privKeyPath := filepath.Join(tempDir, "gitea."+keyType) pubKeyPath := filepath.Join(tempDir, "gitea."+keyType+".pub") - info, err := os.Stat(privKeyPath) + data, err := os.ReadFile(privKeyPath) require.NoError(t, err) - metadata[privKeyPath] = info + content[privKeyPath] = data - info, err = os.Stat(pubKeyPath) + data, err = os.ReadFile(pubKeyPath) require.NoError(t, err) - metadata[pubKeyPath] = info + content[pubKeyPath] = data } // Test recreation on missing private key and noop for missing pub key @@ -98,26 +99,26 @@ func TestInitKeys(t *testing.T) { privKeyPath := filepath.Join(tempDir, "gitea."+keyType) pubKeyPath := filepath.Join(tempDir, "gitea."+keyType+".pub") - infoPriv, err := os.Stat(privKeyPath) + dataPriv, err := os.ReadFile(privKeyPath) require.NoError(t, err) switch keyType { case "rsa": // No modification to RSA key - infoPub, err := os.Stat(pubKeyPath) + dataPub, err := os.ReadFile(pubKeyPath) require.NoError(t, err) - assert.Equal(t, metadata[privKeyPath], infoPriv) - assert.Equal(t, metadata[pubKeyPath], infoPub) + assert.Equal(t, content[privKeyPath], dataPriv) + assert.Equal(t, content[pubKeyPath], dataPub) case "ecdsa": // ECDSA public key should be missing, private unchanged - assert.Equal(t, metadata[privKeyPath], infoPriv) + assert.Equal(t, content[privKeyPath], dataPriv) assert.NoFileExists(t, pubKeyPath) case "ed25519": // ed25519 private key was removed, so both keys regenerated - infoPub, err := os.Stat(pubKeyPath) + dataPub, err := os.ReadFile(pubKeyPath) require.NoError(t, err) - assert.NotEqual(t, metadata[privKeyPath], infoPriv) - assert.NotEqual(t, metadata[pubKeyPath], infoPub) + assert.NotEqual(t, content[privKeyPath], dataPriv) + assert.NotEqual(t, content[pubKeyPath], dataPub) } } } diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 565d298616..310dd6da5e 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -6,6 +6,7 @@ package storage import ( "context" "crypto/tls" + "errors" "fmt" "io" "net/http" @@ -47,40 +48,42 @@ type MinioStorage struct { basePath string } -func convertMinioErr(err error) error { +func convertMinioErr(err error, optMsg ...string) error { if err == nil { return nil } - errResp, ok := err.(minio.ErrorResponse) + + wrapErr := func(err error) error { + if len(optMsg) == 0 { + return err + } + return fmt.Errorf("%s: %w", optMsg[0], err) + } + + errResp, ok := errors.AsType[minio.ErrorResponse](err) if !ok { - return err + return wrapErr(err) } // Convert two responses to standard analogues switch errResp.Code { case "NoSuchKey": - return os.ErrNotExist + return wrapErr(os.ErrNotExist) case "AccessDenied": - return os.ErrPermission + return wrapErr(os.ErrPermission) } - return err -} - -var getBucketVersioning = func(ctx context.Context, minioClient *minio.Client, bucket string) error { - _, err := minioClient.GetBucketVersioning(ctx, bucket) - return err + return wrapErr(err) } // NewMinioStorage returns a minio storage func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage, error) { config := cfg.MinioConfig + log.Info("Creating minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath) if config.ChecksumAlgorithm != "" && config.ChecksumAlgorithm != "default" && config.ChecksumAlgorithm != "md5" { return nil, fmt.Errorf("invalid minio checksum algorithm: %s", config.ChecksumAlgorithm) } - log.Info("Creating Minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath) - var lookup minio.BucketLookupType switch config.BucketLookUpType { case "auto", "": @@ -93,6 +96,13 @@ func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage, return nil, fmt.Errorf("invalid minio bucket lookup type: %s", config.BucketLookUpType) } + // The request error message is something like: + // * "The request signature we calculated does not match the signature you provided. Check your key and signing method." + // It doesn't contain useful information to site admin, so here we wrap the error with our error message + // to tell the site admin what is the problem. + makeErrMsg := func(hint string) string { + return fmt.Sprintf("ObjectStorage.%s: endpoint=%s, location=%s, bucket=%s", hint, config.Endpoint, config.Location, config.Bucket) + } minioClient, err := minio.New(config.Endpoint, &minio.Options{ Creds: buildMinioCredentials(config), Secure: config.UseSSL, @@ -101,37 +111,21 @@ func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage, BucketLookup: lookup, }) if err != nil { - return nil, convertMinioErr(err) - } - - // The GetBucketVersioning is only used for checking whether the Object Storage parameters are generally good. It doesn't need to succeed. - // The assumption is that if the API returns the HTTP code 400, then the parameters could be incorrect. - // Otherwise even if the request itself fails (403, 404, etc), the code should still continue because the parameters seem "good" enough. - // Keep in mind that GetBucketVersioning requires "owner" to really succeed, so it can't be used to check the existence. - // Not using "BucketExists (HeadBucket)" because it doesn't include detailed failure reasons. - err = getBucketVersioning(ctx, minioClient, config.Bucket) - if err != nil { - errResp, ok := err.(minio.ErrorResponse) - if !ok { - return nil, err - } - if errResp.StatusCode == http.StatusBadRequest { - log.Error("S3 storage connection failure at %s:%s with base path %s and region: %s", config.Endpoint, config.Bucket, config.Location, errResp.Message) - return nil, err - } + return nil, convertMinioErr(err, makeErrMsg("NewClient")) } // Check to see if we already own this bucket exists, err := minioClient.BucketExists(ctx, config.Bucket) if err != nil { - return nil, convertMinioErr(err) + return nil, convertMinioErr(err, makeErrMsg("BucketExists")) } + // If the bucket doesn't exist, try to create one if !exists { if err := minioClient.MakeBucket(ctx, config.Bucket, minio.MakeBucketOptions{ Region: config.Location, }); err != nil { - return nil, convertMinioErr(err) + return nil, convertMinioErr(err, makeErrMsg("MakeBucket")) } } diff --git a/modules/storage/minio_test.go b/modules/storage/minio_test.go index 1bb772c8d3..67c4d18933 100644 --- a/modules/storage/minio_test.go +++ b/modules/storage/minio_test.go @@ -4,16 +4,13 @@ package storage import ( - "context" "net/http" "net/http/httptest" - "os" "testing" "gitea.dev/modules/setting" "gitea.dev/modules/test" - "github.com/minio/minio-go/v7" "github.com/stretchr/testify/assert" ) @@ -84,31 +81,18 @@ func TestMinioStoragePath(t *testing.T) { } func TestS3StorageBadRequest(t *testing.T) { - if os.Getenv("CI") == "" { - t.Skip("S3Storage not present outside of CI") - return - } + endpoint := test.ExternalServiceHTTP(t, "TEST_MINIO_ENDPOINT", "minio:9000") cfg := &setting.Storage{ MinioConfig: setting.MinioStorageConfig{ - Endpoint: "minio:9000", + Endpoint: endpoint, AccessKeyID: "123456", - SecretAccessKey: "12345678", + SecretAccessKey: "invalid-secret", Bucket: "bucket", Location: "us-east-1", }, } - message := "ERROR" - old := getBucketVersioning - defer func() { getBucketVersioning = old }() - getBucketVersioning = func(ctx context.Context, minioClient *minio.Client, bucket string) error { - return minio.ErrorResponse{ - StatusCode: http.StatusBadRequest, - Code: "FixtureError", - Message: message, - } - } _, err := NewStorage(setting.MinioStorageType, cfg) - assert.ErrorContains(t, err, message) + assert.ErrorContains(t, err, "ObjectStorage.BucketExists: endpoint="+endpoint) } func TestMinioCredentials(t *testing.T) { diff --git a/modules/structs/admin_user.go b/modules/structs/admin_user.go index ee65e016bc..4fd10aa580 100644 --- a/modules/structs/admin_user.go +++ b/modules/structs/admin_user.go @@ -19,7 +19,6 @@ type CreateUserOption struct { // The full display name of the user FullName string `json:"full_name" binding:"MaxSize(100)"` // required: true - // swagger:strfmt email Email string `json:"email" binding:"Required;Email;MaxSize(254)"` // The plain text password for the user Password string `json:"password" binding:"MaxSize(255)"` @@ -30,7 +29,7 @@ type CreateUserOption struct { // Whether the user has restricted access privileges Restricted *bool `json:"restricted"` // User visibility level: public, limited, or private - Visibility UserVisibility `json:"visibility" binding:"In(,public,limited,private)"` + Visibility VisibilityString `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 +78,5 @@ type EditUserOption struct { // Whether the user has restricted access privileges Restricted *bool `json:"restricted"` // User visibility level: public, limited, or private - Visibility UserVisibility `json:"visibility" binding:"In(,public,limited,private)"` + Visibility VisibilityString `json:"visibility" binding:"In(,public,limited,private)"` } diff --git a/modules/structs/issue.go b/modules/structs/issue.go index 54e62ad2bb..07c82a7b4b 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -133,7 +133,6 @@ type IssueAssigneesOption struct { // EditDeadlineOption options for creating a deadline type EditDeadlineOption struct { // required:true - // swagger:strfmt date-time Deadline *time.Time `json:"due_date"` } diff --git a/modules/structs/notifications.go b/modules/structs/notifications.go index b94e02aee3..20232486a9 100644 --- a/modules/structs/notifications.go +++ b/modules/structs/notifications.go @@ -68,7 +68,7 @@ const ( type NotifySubjectType string const ( - // NotifySubjectIssue a issue is subject of an notification + // NotifySubjectIssue an issue is subject of a notification NotifySubjectIssue NotifySubjectType = "Issue" // NotifySubjectPull a pull is subject of an notification NotifySubjectPull NotifySubjectType = "Pull" diff --git a/modules/structs/org.go b/modules/structs/org.go index b93f68b600..162a47904f 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 UserVisibility `json:"visibility"` + Visibility VisibilityString `json:"visibility"` // Whether repository administrators can change team access RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` // username of the organization @@ -60,7 +60,7 @@ type CreateOrgOption struct { // The location of the organization Location string `json:"location" binding:"MaxSize(50)"` // possible values are `public` (default), `limited` or `private` - Visibility UserVisibility `json:"visibility" binding:"In(,public,limited,private)"` + Visibility VisibilityString `json:"visibility" binding:"In(,public,limited,private)"` // Whether repository administrators can change team access RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` } @@ -78,7 +78,7 @@ type EditOrgOption struct { // The location of the organization Location *string `json:"location" binding:"MaxSize(50)"` // possible values are `public`, `limited` or `private` - Visibility *UserVisibility `json:"visibility" binding:"In(,public,limited,private)"` + Visibility *VisibilityString `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 c9542951a0..02f806aadb 100644 --- a/modules/structs/org_team.go +++ b/modules/structs/org_team.go @@ -54,10 +54,10 @@ type CreateTeamOption struct { // Whether the team has access to all repositories in the organization 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"] + // example: ["repo.actions","repo.packages","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"` - // example: {"repo.actions","repo.packages","repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.wiki":"admin","repo.pulls":"owner","repo.releases":"none","repo.projects":"none","repo.ext_wiki":"none"} + // example: {"repo.actions":"read","repo.packages":"read","repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.wiki":"admin","repo.pulls":"owner","repo.releases":"none","repo.projects":"none","repo.ext_wiki":"none"} UnitsMap map[string]string `json:"units_map"` // Whether the team can create repositories in the organization CanCreateOrgRepo bool `json:"can_create_org_repo"` diff --git a/modules/structs/user.go b/modules/structs/user.go index 852d2c4bdd..1c1fc68f8b 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 UserVisibility `json:"visibility"` + Visibility VisibilityString `json:"visibility"` // user counts Followers int `json:"followers_count"` diff --git a/modules/structs/visible_type.go b/modules/structs/visible_type.go index f537884963..5031809e74 100644 --- a/modules/structs/visible_type.go +++ b/modules/structs/visible_type.go @@ -18,10 +18,10 @@ const ( ) // VisibilityModes is a map of Visibility types -var VisibilityModes = map[string]VisibleType{ - "public": VisibleTypePublic, - "limited": VisibleTypeLimited, - "private": VisibleTypePrivate, +var VisibilityModes = map[VisibilityString]VisibleType{ + VisibilityStringPublic: VisibleTypePublic, + VisibilityStringLimited: VisibleTypeLimited, + VisibilityStringPrivate: VisibleTypePrivate, } // IsPublic returns true if VisibleType is public @@ -43,27 +43,27 @@ func (vt VisibleType) IsPrivate() bool { func (vt VisibleType) String() string { for k, v := range VisibilityModes { if vt == v { - return k + return string(k) } } return "" } -// ExtractKeysFromMapString provides a slice of keys from map -func ExtractKeysFromMapString(in map[string]VisibleType) (keys []string) { - for k := range in { - keys = append(keys, k) +// VisibilityModeKeys returns the visibility mode names (public, limited, private) +func VisibilityModeKeys() (keys []string) { + for k := range VisibilityModes { + keys = append(keys, string(k)) } 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 +// VisibilityString defines the visibility level of a user/organization/team as +// rendered in API and config payloads. The DB representation is VisibleType (int). +// swagger:enum VisibilityString +type VisibilityString string const ( - UserVisibilityPublic UserVisibility = "public" - UserVisibilityLimited UserVisibility = "limited" - UserVisibilityPrivate UserVisibility = "private" + VisibilityStringPublic VisibilityString = "public" + VisibilityStringLimited VisibilityString = "limited" + VisibilityStringPrivate VisibilityString = "private" ) diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go index 27020e6e68..f04724bfe6 100644 --- a/modules/templates/helper_test.go +++ b/modules/templates/helper_test.go @@ -92,9 +92,12 @@ func TestTemplateEscape(t *testing.T) { } t.Run("Golang URL Escape", func(t *testing.T) { - // Golang template considers "href", "*src*", "*uri*", "*url*" (and more) ... attributes as contentTypeURL and does auto-escaping + // HINT: GOLANG-HTML-TEMPLATE-URL-ESCAPING: demo cases (html/template/attr.go): + // Golang template considers "href", "data-href", "*src*", "*uri*", "*url*" (and more) ... attributes as contentTypeURL and does auto-escaping actual := execTmpl(``) assert.Equal(t, ``, actual) + actual = execTmpl(``) + assert.Equal(t, ``, actual) actual = execTmpl(``) assert.Equal(t, ``, actual) }) @@ -102,6 +105,10 @@ func TestTemplateEscape(t *testing.T) { // non-URL content isn't auto-escaped actual := execTmpl(``) assert.Equal(t, ``, actual) + // the attr names like "data-href" and "data-action" are treated as URL (as the "data-" prefix is stripped) + // but "data-xxx-href" and "data-xxx-action" are not, so no escaping. + actual = execTmpl(``) + assert.Equal(t, ``, actual) }) t.Run("QueryBuild", func(t *testing.T) { actual := execTmpl(``) diff --git a/modules/turnstile/turnstile.go b/modules/turnstile/turnstile.go index 381cb71fda..6bae89caee 100644 --- a/modules/turnstile/turnstile.go +++ b/modules/turnstile/turnstile.go @@ -12,9 +12,20 @@ import ( "strings" "gitea.dev/modules/json" + "gitea.dev/modules/proxy" "gitea.dev/modules/setting" + "gitea.dev/modules/util" ) +// httpClient returns an HTTP client that honors Gitea's proxy configuration. +var httpClient = util.OnceValue[*http.Client]{ + Func: func() *http.Client { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.Proxy = proxy.Proxy() + return &http.Client{Transport: transport} + }, +} + // Response is the structure of JSON returned from API type Response struct { Success bool `json:"success"` @@ -40,7 +51,7 @@ func Verify(ctx context.Context, response string) (bool, error) { } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Value().Do(req) if err != nil { return false, fmt.Errorf("Failed to send CAPTCHA response: %w", err) } diff --git a/modules/turnstile/turnstile_test.go b/modules/turnstile/turnstile_test.go new file mode 100644 index 0000000000..a997e4c32a --- /dev/null +++ b/modules/turnstile/turnstile_test.go @@ -0,0 +1,38 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package turnstile + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "gitea.dev/modules/setting" + "gitea.dev/modules/test" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHTTPClientHonorsProxy(t *testing.T) { + proxyURL, err := url.Parse("http://proxy.example.com:3128") + require.NoError(t, err) + + defer test.MockVariableValue(&setting.Proxy.Enabled, true)() + defer test.MockVariableValue(&setting.Proxy.ProxyURL, proxyURL.String())() + defer test.MockVariableValue(&setting.Proxy.ProxyURLFixed, proxyURL)() + defer test.MockVariableValue(&setting.Proxy.ProxyHosts, []string{"**"})() + httpClient.Reset() + transport, ok := httpClient.Value().Transport.(*http.Transport) + require.True(t, ok) + require.NotNil(t, transport.Proxy) + + // The Turnstile verification request must be routed through the configured proxy. + req := httptest.NewRequest(http.MethodPost, "https://any.example.com", nil) + got, err := transport.Proxy(req) + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, proxyURL.String(), got.String()) +} diff --git a/modules/uri/uri.go b/modules/uri/uri.go index 768bc662ce..2dc4f0c949 100644 --- a/modules/uri/uri.go +++ b/modules/uri/uri.go @@ -23,13 +23,21 @@ func (e ErrURISchemeNotSupported) Error() string { // Open open a local file or a remote file func Open(uriStr string) (io.ReadCloser, error) { + return OpenWithClient(uriStr, http.DefaultClient) +} + +// OpenWithClient opens a local file or a remote file, using the given (non-nil) HTTP client +// for http/https URLs. Callers that must confine remote access (e.g. to defeat SSRF via +// redirects) should pass a client whose transport validates the peer at dial time; Open +// passes http.DefaultClient. +func OpenWithClient(uriStr string, client *http.Client) (io.ReadCloser, error) { u, err := url.Parse(uriStr) if err != nil { return nil, err } switch strings.ToLower(u.Scheme) { case "http", "https": - f, err := http.Get(uriStr) + f, err := client.Get(uriStr) if err != nil { return nil, err } diff --git a/modules/uri/uri_test.go b/modules/uri/uri_test.go index 11b915c261..bfd1a3b526 100644 --- a/modules/uri/uri_test.go +++ b/modules/uri/uri_test.go @@ -4,10 +4,18 @@ package uri import ( + "context" + "errors" + "net" + "net/http" + "net/http/httptest" "path/filepath" + "strings" + "sync/atomic" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestReadURI(t *testing.T) { @@ -17,3 +25,45 @@ func TestReadURI(t *testing.T) { assert.NoError(t, err) defer f.Close() } + +// TestOpenWithClientValidatesRedirectTarget verifies OpenWithClient routes the +// whole request chain (including redirects) through the provided client, so a +// client whose transport refuses to dial an internal target blocks a redirect to +// it — whereas the default client (old Open behavior) follows it. +func TestOpenWithClientValidatesRedirectTarget(t *testing.T) { + var internalHit atomic.Bool + internal := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + internalHit.Store(true) + _, _ = w.Write([]byte("secret")) + })) + defer internal.Close() + + front := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, internal.URL, http.StatusFound) + })) + defer front.Close() + + internalAddr := strings.TrimPrefix(internal.URL, "http://") + + // a client that refuses to dial the internal target, mimicking the migration + // hostmatcher dialer that re-validates every hop + blockingClient := &http.Client{Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + if addr == internalAddr { + return nil, errors.New("blocked internal address") + } + return (&net.Dialer{}).DialContext(ctx, network, addr) + }, + }} + + _, err := OpenWithClient(front.URL, blockingClient) + require.Error(t, err) + assert.False(t, internalHit.Load(), "the redirect target must not be reached through the validating client") + + // the default client (the previous behavior) follows the redirect to the internal target + internalHit.Store(false) + rc, err := Open(front.URL) + require.NoError(t, err) + _ = rc.Close() + assert.True(t, internalHit.Load(), "sanity check: the default client follows the redirect") +} diff --git a/modules/util/sync.go b/modules/util/sync.go new file mode 100644 index 0000000000..447bfe10f4 --- /dev/null +++ b/modules/util/sync.go @@ -0,0 +1,51 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package util + +import ( + "sync" + "sync/atomic" +) + +type onceValueResult[T any] struct { + value T + panic any +} + +// OnceValue is similar to Golang's "sync.OnceValue", but can be reset. +type OnceValue[T any] struct { + Func func() T + mu sync.Mutex + res atomic.Pointer[onceValueResult[T]] +} + +func (o *OnceValue[T]) Value() T { + res := o.res.Load() + if res == nil { + o.mu.Lock() + defer o.mu.Unlock() + res = o.res.Load() + if res == nil { + res = &onceValueResult[T]{} + defer func() { + res.panic = recover() + o.res.Store(res) + if res.panic != nil { + panic(res.panic) + } + }() + res.value = o.Func() + } + } + if res.panic != nil { + panic(res.panic) + } + return res.value +} + +func (o *OnceValue[T]) Reset() { + o.mu.Lock() + defer o.mu.Unlock() + o.res.Store(nil) +} diff --git a/modules/util/sync_test.go b/modules/util/sync_test.go new file mode 100644 index 0000000000..297328bd91 --- /dev/null +++ b/modules/util/sync_test.go @@ -0,0 +1,49 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOnceValue(t *testing.T) { + t.Run("RepeatCall", func(t *testing.T) { + callCount := 0 + o := OnceValue[int]{Func: func() int { + callCount++ + return 42 + }} + assert.Equal(t, 42, o.Value()) + assert.Equal(t, 42, o.Value()) + assert.Equal(t, 1, callCount) + o.Reset() + assert.Equal(t, 42, o.Value()) + assert.Equal(t, 2, callCount) + assert.Equal(t, 42, o.Value()) + assert.Equal(t, 2, callCount) + }) + + t.Run("Panic", func(t *testing.T) { + callCount := 0 + doPanic := true + o := OnceValue[int]{Func: func() int { + callCount++ + if doPanic { + panic("some error") + } + return 42 + }} + assert.PanicsWithValue(t, "some error", func() { o.Value() }) + assert.PanicsWithValue(t, "some error", func() { o.Value() }) + assert.Equal(t, 1, callCount) + doPanic = false + o.Reset() + assert.Equal(t, 42, o.Value()) + assert.Equal(t, 2, callCount) + assert.Equal(t, 42, o.Value()) + assert.Equal(t, 2, callCount) + }) +} diff --git a/modules/util/time_str.go b/modules/util/time_str.go index 81b132c3db..c498e6ebe1 100644 --- a/modules/util/time_str.go +++ b/modules/util/time_str.go @@ -40,6 +40,7 @@ var timeStrGlobalVars = sync.OnceValue(func() *timeStrGlobalVarsType { }) func TimeEstimateParse(timeStr string) (int64, error) { + timeStr = strings.TrimSpace(timeStr) if timeStr == "" { return 0, nil } @@ -51,7 +52,13 @@ func TimeEstimateParse(timeStr string) (int64, error) { if matches[0][0] != 0 || matches[len(matches)-1][1] != len(timeStr) { return 0, fmt.Errorf("invalid time string: %s", timeStr) } + prevEnd := 0 for _, match := range matches { + // only whitespace may separate two units, otherwise the string contains invalid content like "1h x 2m" + if strings.TrimSpace(timeStr[prevEnd:match[0]]) != "" { + return 0, fmt.Errorf("invalid time string: %s", timeStr) + } + prevEnd = match[1] amount, err := strconv.ParseInt(timeStr[match[2]:match[3]], 10, 64) if err != nil { return 0, fmt.Errorf("invalid time string: %v", err) diff --git a/modules/util/time_str_test.go b/modules/util/time_str_test.go index 8d1de51c8e..4efe902bee 100644 --- a/modules/util/time_str_test.go +++ b/modules/util/time_str_test.go @@ -22,6 +22,9 @@ func TestTimeStr(t *testing.T) { {"1s", 1, false}, {"1h 1m 1s", 3600 + 60 + 1, false}, {"1d1x", 0, true}, + {"1h 2x 3m", 0, true}, + {"1h_2m", 0, true}, + {"1h,1m", 0, true}, } for _, test := range tests { t.Run(test.input, func(t *testing.T) { diff --git a/modules/web/middleware/locale.go b/modules/web/middleware/locale.go index c376c21865..572f938aff 100644 --- a/modules/web/middleware/locale.go +++ b/modules/web/middleware/locale.go @@ -12,6 +12,24 @@ import ( "golang.org/x/text/language" ) +// maxAcceptLanguageLen bounds the Accept-Language header before it reaches +// language.ParseAcceptLanguage. That parser has quadratic-time behavior on long +// malformed inputs, and its built-in guard only counts "-" separators while the +// scanner treats "_" as an alias for "-", so a "_"-heavy header slips past the +// guard and burns CPU. Only the leading (highest-priority) languages are used, so +// truncating a longer header is safe. +const maxAcceptLanguageLen = 200 + +// parseAcceptLanguage parses the Accept-Language header after bounding its length +// to avoid a quadratic-time DoS on attacker-controlled input. +func parseAcceptLanguage(header string) []language.Tag { + if len(header) > maxAcceptLanguageLen { + header = header[:maxAcceptLanguageLen] + } + tags, _, _ := language.ParseAcceptLanguage(header) + return tags +} + // Locale handle locale func Locale(resp http.ResponseWriter, req *http.Request) translation.Locale { // 1. Check URL arguments. @@ -35,7 +53,7 @@ func Locale(resp http.ResponseWriter, req *http.Request) translation.Locale { // 3. Get language information from 'Accept-Language'. // The first element in the list is chosen to be the default language automatically. if len(lang) == 0 { - tags, _, _ := language.ParseAcceptLanguage(req.Header.Get("Accept-Language")) + tags := parseAcceptLanguage(req.Header.Get("Accept-Language")) tag := translation.Match(tags...) lang = tag.String() } diff --git a/modules/web/middleware/locale_test.go b/modules/web/middleware/locale_test.go new file mode 100644 index 0000000000..355ac94338 --- /dev/null +++ b/modules/web/middleware/locale_test.go @@ -0,0 +1,27 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package middleware + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseAcceptLanguage(t *testing.T) { + // a normal header is parsed and its leading language preserved + tags := parseAcceptLanguage("de-DE,de;q=0.9,en;q=0.8") + assert.NotEmpty(t, tags) + assert.Equal(t, "de-DE", tags[0].String()) + + // an oversized "_"-separated header would drive ParseAcceptLanguage into its + // quadratic-time path (the built-in guard only counts "-"); the length bound + // keeps the input passed to the parser small so it cannot be used for a DoS. + malicious := strings.Repeat("_aaaaaaaaa", 1<<16) // ~640 KiB, zero "-" characters + assert.Greater(t, len(malicious), maxAcceptLanguageLen) + tags = parseAcceptLanguage(malicious) + // no panic / hang, and nothing meaningful is parsed out of the garbage + assert.Empty(t, tags) +} diff --git a/options/fileicon/material-icon-rules.json b/options/fileicon/material-icon-rules.json index aa967b7100..c0c3a8d380 100644 --- a/options/fileicon/material-icon-rules.json +++ b/options/fileicon/material-icon-rules.json @@ -5,6 +5,11 @@ "_rust": "folder-rust", "-rust": "folder-rust", "__rust__": "folder-rust", + "cargo": "folder-rust", + ".cargo": "folder-rust", + "_cargo": "folder-rust", + "-cargo": "folder-rust", + "__cargo__": "folder-rust", "bot": "folder-robot", ".bot": "folder-robot", "_bot": "folder-robot", @@ -350,6 +355,11 @@ "_scripting": "folder-scripts", "-scripting": "folder-scripts", "__scripting__": "folder-scripts", + "xtask": "folder-scripts", + ".xtask": "folder-scripts", + "_xtask": "folder-scripts", + "-xtask": "folder-scripts", + "__xtask__": "folder-scripts", "node": "folder-node", ".node": "folder-node", "_node": "folder-node", @@ -1020,6 +1030,11 @@ "_externals": "folder-lib", "-externals": "folder-lib", "__externals__": "folder-lib", + "crates": "folder-lib", + ".crates": "folder-lib", + "_crates": "folder-lib", + "-crates": "folder-lib", + "__crates__": "folder-lib", "themes": "folder-theme", ".themes": "folder-theme", "_themes": "folder-theme", @@ -4414,22 +4429,18 @@ "_organism": "folder-organism", "-organism": "folder-organism", "__organism__": "folder-organism", + "claude": "folder-claude", ".claude": "folder-claude", - "..claude": "folder-claude", - "_.claude": "folder-claude", - "-.claude": "folder-claude", - "__.claude__": "folder-claude", + "_claude": "folder-claude", + "-claude": "folder-claude", + "__claude__": "folder-claude", + "cursor": "folder-cursor", ".cursor": "folder-cursor", - "..cursor": "folder-cursor", - "_.cursor": "folder-cursor", - "-.cursor": "folder-cursor", - "__.cursor__": "folder-cursor", - ".gemini": "folder-gemini-ai", - "..gemini": "folder-gemini-ai", - "_.gemini": "folder-gemini-ai", - "-.gemini": "folder-gemini-ai", - "__.gemini__": "folder-gemini-ai", + "_cursor": "folder-cursor", + "-cursor": "folder-cursor", + "__cursor__": "folder-cursor", "gemini": "folder-gemini-ai", + ".gemini": "folder-gemini-ai", "_gemini": "folder-gemini-ai", "-gemini": "folder-gemini-ai", "__gemini__": "folder-gemini-ai", @@ -4443,6 +4454,11 @@ "_geminiai": "folder-gemini-ai", "-geminiai": "folder-gemini-ai", "__geminiai__": "folder-gemini-ai", + "opencode": "folder-opencode", + ".opencode": "folder-opencode", + "_opencode": "folder-opencode", + "-opencode": "folder-opencode", + "__opencode__": "folder-opencode", "input": "folder-input", ".input": "folder-input", "_input": "folder-input", @@ -4583,6 +4599,11 @@ "_instructions": "folder-instructions", "-instructions": "folder-instructions", "__instructions__": "folder-instructions", + "zed": "folder-zed", + ".zed": "folder-zed", + "_zed": "folder-zed", + "-zed": "folder-zed", + "__zed__": "folder-zed", "appwrite": "folder-appwrite", ".appwrite": "folder-appwrite", "_appwrite": "folder-appwrite", @@ -4617,7 +4638,22 @@ ".kotlin": "folder-kotlin", "_kotlin": "folder-kotlin", "-kotlin": "folder-kotlin", - "__kotlin__": "folder-kotlin" + "__kotlin__": "folder-kotlin", + "redis": "folder-redis", + ".redis": "folder-redis", + "_redis": "folder-redis", + "-redis": "folder-redis", + "__redis__": "folder-redis", + "redis-db": "folder-redis", + ".redis-db": "folder-redis", + "_redis-db": "folder-redis", + "-redis-db": "folder-redis", + "__redis-db__": "folder-redis", + "redislabs": "folder-redis", + ".redislabs": "folder-redis", + "_redislabs": "folder-redis", + "-redislabs": "folder-redis", + "__redislabs__": "folder-redis" }, "folderNamesExpanded": { "rust": "folder-rust-open", @@ -4625,6 +4661,11 @@ "_rust": "folder-rust-open", "-rust": "folder-rust-open", "__rust__": "folder-rust-open", + "cargo": "folder-rust-open", + ".cargo": "folder-rust-open", + "_cargo": "folder-rust-open", + "-cargo": "folder-rust-open", + "__cargo__": "folder-rust-open", "bot": "folder-robot-open", ".bot": "folder-robot-open", "_bot": "folder-robot-open", @@ -4970,6 +5011,11 @@ "_scripting": "folder-scripts-open", "-scripting": "folder-scripts-open", "__scripting__": "folder-scripts-open", + "xtask": "folder-scripts-open", + ".xtask": "folder-scripts-open", + "_xtask": "folder-scripts-open", + "-xtask": "folder-scripts-open", + "__xtask__": "folder-scripts-open", "node": "folder-node-open", ".node": "folder-node-open", "_node": "folder-node-open", @@ -5640,6 +5686,11 @@ "_externals": "folder-lib-open", "-externals": "folder-lib-open", "__externals__": "folder-lib-open", + "crates": "folder-lib-open", + ".crates": "folder-lib-open", + "_crates": "folder-lib-open", + "-crates": "folder-lib-open", + "__crates__": "folder-lib-open", "themes": "folder-theme-open", ".themes": "folder-theme-open", "_themes": "folder-theme-open", @@ -9034,22 +9085,18 @@ "_organism": "folder-organism-open", "-organism": "folder-organism-open", "__organism__": "folder-organism-open", + "claude": "folder-claude-open", ".claude": "folder-claude-open", - "..claude": "folder-claude-open", - "_.claude": "folder-claude-open", - "-.claude": "folder-claude-open", - "__.claude__": "folder-claude-open", + "_claude": "folder-claude-open", + "-claude": "folder-claude-open", + "__claude__": "folder-claude-open", + "cursor": "folder-cursor-open", ".cursor": "folder-cursor-open", - "..cursor": "folder-cursor-open", - "_.cursor": "folder-cursor-open", - "-.cursor": "folder-cursor-open", - "__.cursor__": "folder-cursor-open", - ".gemini": "folder-gemini-ai-open", - "..gemini": "folder-gemini-ai-open", - "_.gemini": "folder-gemini-ai-open", - "-.gemini": "folder-gemini-ai-open", - "__.gemini__": "folder-gemini-ai-open", + "_cursor": "folder-cursor-open", + "-cursor": "folder-cursor-open", + "__cursor__": "folder-cursor-open", "gemini": "folder-gemini-ai-open", + ".gemini": "folder-gemini-ai-open", "_gemini": "folder-gemini-ai-open", "-gemini": "folder-gemini-ai-open", "__gemini__": "folder-gemini-ai-open", @@ -9063,6 +9110,11 @@ "_geminiai": "folder-gemini-ai-open", "-geminiai": "folder-gemini-ai-open", "__geminiai__": "folder-gemini-ai-open", + "opencode": "folder-opencode-open", + ".opencode": "folder-opencode-open", + "_opencode": "folder-opencode-open", + "-opencode": "folder-opencode-open", + "__opencode__": "folder-opencode-open", "input": "folder-input-open", ".input": "folder-input-open", "_input": "folder-input-open", @@ -9203,6 +9255,11 @@ "_instructions": "folder-instructions-open", "-instructions": "folder-instructions-open", "__instructions__": "folder-instructions-open", + "zed": "folder-zed-open", + ".zed": "folder-zed-open", + "_zed": "folder-zed-open", + "-zed": "folder-zed-open", + "__zed__": "folder-zed-open", "appwrite": "folder-appwrite-open", ".appwrite": "folder-appwrite-open", "_appwrite": "folder-appwrite-open", @@ -9237,7 +9294,22 @@ ".kotlin": "folder-kotlin-open", "_kotlin": "folder-kotlin-open", "-kotlin": "folder-kotlin-open", - "__kotlin__": "folder-kotlin-open" + "__kotlin__": "folder-kotlin-open", + "redis": "folder-redis-open", + ".redis": "folder-redis-open", + "_redis": "folder-redis-open", + "-redis": "folder-redis-open", + "__redis__": "folder-redis-open", + "redis-db": "folder-redis-open", + ".redis-db": "folder-redis-open", + "_redis-db": "folder-redis-open", + "-redis-db": "folder-redis-open", + "__redis-db__": "folder-redis-open", + "redislabs": "folder-redis-open", + ".redislabs": "folder-redis-open", + "_redislabs": "folder-redis-open", + "-redislabs": "folder-redis-open", + "__redislabs__": "folder-redis-open" }, "rootFolderNames": {}, "rootFolderNamesExpanded": {}, @@ -9247,11 +9319,29 @@ "xhtml": "html", "html_vm": "html", "asp": "html", + "html": "html", + "aspx": "html", + "jshtm": "html", + "rhtml": "html", + "shtml": "html", + "volt": "html", + "xht": "html", "jade": "pug", "pug": "pug", "md": "markdown", "markdown": "markdown", "rst": "markdown", + "copilotmd": "markdown", + "litcoffee": "markdown", + "markdn": "markdown", + "mdown": "markdown", + "mdtext": "markdown", + "mdtxt": "markdown", + "mdwn": "markdown", + "mkd": "markdown", + "mkdn": "markdown", + "ronn": "markdown", + "workbook": "markdown", "blink": "blink", "css": "css", "scss": "sass", @@ -9263,6 +9353,11 @@ "json5": "json", "jsonl": "json", "ndjson": "json", + "geojson": "json", + "har": "json", + "jsonld": "json", + "webmanifest": "json", + "ts.map": "json", "schema.json": "json_schema", "hjson": "hjson", "jinja": "jinja", @@ -9281,6 +9376,14 @@ "yml.dist": "yaml", "yaml.dist": "yaml", "yaml-tmlanguage": "yaml", + "yaml": "yaml", + "yml": "yaml", + "cff": "yaml", + "eyaml": "yaml", + "eyml": "yaml", + "winget": "yaml", + "yaml-tmpreferences": "yaml", + "yaml-tmtheme": "yaml", "xml": "xml", "plist": "xml", "xsd": "xml", @@ -9298,6 +9401,48 @@ "dmn": "xml", "jrxml": "xml", "xmp": "xml", + "ascx": "xml", + "atom": "xml", + "axaml": "xml", + "axml": "xml", + "bpmn": "xml", + "csl": "xml", + "csproj.user": "xml", + "dita": "xml", + "ditamap": "xml", + "dtml": "xml", + "ent": "xml", + "fxml": "xml", + "isml": "xml", + "jmx": "xml", + "launch": "xml", + "menu": "xml", + "opml": "xml", + "owl": "xml", + "proj": "xml", + "publishsettings": "xml", + "pubxml": "xml", + "pubxml.user": "xml", + "rdf": "xml", + "rng": "xml", + "rss": "xml", + "shproj": "xml", + "storyboard": "xml", + "targets": "xml", + "tld": "xml", + "tmx": "xml", + "vbproj": "xml", + "vbproj.user": "xml", + "wsdl": "xml", + "wxi": "xml", + "wxl": "xml", + "wxs": "xml", + "xbl": "xml", + "xib": "xml", + "xliff": "xml", + "xoml": "xml", + "xpdl": "xml", + "xul": "xml", "toml": "toml", "toon": "toon", "png": "image", @@ -9322,6 +9467,7 @@ "jbig2": "image", "jb2": "image", "jng": "image", + "jxl": "image", "jxr": "image", "pgf": "image", "pic": "image", @@ -9397,6 +9543,10 @@ "act": "palette", "esx": "javascript", "mjs": "javascript", + "js": "javascript", + "cjs": "javascript", + "es6": "javascript", + "pac": "javascript", "jsx": "react", "tsx": "react_ts", "routing.ts": "routing", @@ -9426,6 +9576,13 @@ "cfg": "settings", "cnf": "settings", "tool-versions": "settings", + "directory": "settings", + "mak": "settings", + "npmrc": "settings", + "repo": "settings", + "ts": "typescript", + "cts": "typescript", + "mts": "typescript", "d.ts": "typescript-def", "d.cts": "typescript-def", "d.mts": "typescript-def", @@ -9463,6 +9620,9 @@ "vcxproj": "visualstudio", "vcxproj.filters": "visualstudio", "wixproj": "visualstudio", + "bas": "visualstudio", + "brs": "visualstudio", + "vba": "visualstudio", "vcl": "varnish", "pdb": "database", "sql": "database", @@ -9502,6 +9662,7 @@ "ldf": "database", "frm": "database", "kdbx": "database", + "dsql": "database", "kql": "kusto", "cs": "csharp", "csx": "csharp", @@ -9565,6 +9726,7 @@ "hex": "hex", "java": "java", "jsp": "java", + "jav": "java", "jar": "jar", "class": "javaclass", "c3": "c3", @@ -9572,6 +9734,7 @@ "i": "c", "mi": "c", "h": "h", + "hip": "hip", "cc": "cpp", "cpp": "cpp", "cxx": "cpp", @@ -9580,6 +9743,15 @@ "mii": "cpp", "ii": "cpp", "cppm": "cpp", + "c++m": "cpp", + "ccm": "cpp", + "cxxm": "cpp", + "h.in": "cpp", + "hpp.in": "cpp", + "ipp": "cpp", + "ixx": "cpp", + "tpp": "cpp", + "txx": "cpp", "hh": "hpp", "hpp": "hpp", "hxx": "hpp", @@ -9587,9 +9759,19 @@ "hp": "hpp", "tcc": "hpp", "inl": "hpp", + "m": "objective-c", + "mm": "objective-cpp", "rc": "rc", "go": "go", "py": "python", + "cpy": "python", + "gyp": "python", + "gypi": "python", + "ipy": "python", + "pyi": "python", + "pyt": "python", + "pyw": "python", + "rpy": "python", "pyc": "python-misc", "whl": "python-misc", "egg": "python-misc", @@ -9607,12 +9789,34 @@ "exp": "console", "nu": "console", "xsh": "console", + "bash_aliases": "console", + "bash_login": "console", + "bash_logout": "console", + "bash_profile": "console", + "bashrc": "console", + "cshrc": "console", + "ebuild": "console", + "eclass": "console", + "profile": "console", + "tcshrc": "console", + "xprofile": "console", + "xsession": "console", + "xsessionrc": "console", + "yash_profile": "console", + "yashrc": "console", + "zlogin": "console", + "zlogout": "console", + "zprofile": "console", + "zsh-theme": "console", + "zshenv": "console", + "zshrc": "console", "ps1": "powershell", "psm1": "powershell", "psd1": "powershell", "ps1xml": "powershell", "psc1": "powershell", "pssc": "powershell", + "psrc": "powershell", "excalidraw": "excalidraw", "excalidraw.json": "excalidraw", "excalidraw.svg": "excalidraw", @@ -9654,6 +9858,9 @@ "lib": "lib", "a": "lib", "bib": "bibliography", + "bbl": "bibliography", + "bcf": "bibliography", + "blg": "bibliography", "bst": "bibtex-style", "dll": "dll", "ilk": "dll", @@ -9661,10 +9868,18 @@ "rb": "ruby", "erb": "ruby", "rbs": "ruby", + "gemspec": "ruby", + "podspec": "ruby", + "rake": "ruby", + "rbi": "ruby", + "rbx": "ruby", + "rjs": "ruby", + "ru": "ruby", "fs": "fsharp", "fsx": "fsharp", "fsi": "fsharp", "fsproj": "fsharp", + "fsscript": "fsharp", "swift": "swift", "xcplayground": "swift", "swiftdeps": "swift", @@ -9680,6 +9895,15 @@ "containerfile": "docker", "compose.yaml": "docker", "compose.yml": "docker", + "tex": "tex", + "ltx": "tex", + "cls": "tex", + "clo": "tex", + "latex": "tex", + "aux": "tex", + "tikz": "tex", + "synctex": "tex", + "synctex.gz": "tex", "sty": "sty", "ctx": "context", "dtx": "dtx", @@ -9820,13 +10044,23 @@ "mist.tsx": "mist", "otne": "otne", "patch": "git", + "diff": "diff", + "rej": "diff", "lua": "lua", "clj": "clojure", "cljs": "clojure", "cljc": "clojure", + "cljx": "clojure", + "clojure": "clojure", + "edn": "clojure", "groovy": "groovy", + "gvy": "groovy", + "nf": "groovy", "r": "r", "rmd": "r", + "rhistory": "r", + "rprofile": "r", + "rt": "r", "dart": "dart", "freezed.dart": "dart_generated", "g.dart": "dart_generated", @@ -9862,8 +10096,13 @@ "lock": "lock", "hbs": "handlebars", "mustache": "handlebars", + "handlebars": "handlebars", + "hjs": "handlebars", "pm": "perl", "raku": "perl", + "pod": "perl", + "psgi": "perl", + "t": "perl", "hx": "haxe", "spec.ts": "test-ts", "spec.cts": "test-ts", @@ -10074,6 +10313,7 @@ "prisma": "prisma", "cshtml": "razor", "vbhtml": "razor", + "razor": "razor", "abc": "abc", "ad": "asciidoc", "adoc": "asciidoc", @@ -10088,16 +10328,37 @@ "stl": "3d", "stp": "3d", "step": "3d", + "ste": "3d", "obj": "3d", "o": "3d", "ac": "3d", + "dwg": "3d", "dxf": "3d", "fbx": "3d", "mesh": "3d", + "3dm": "3d", + "3mf": "3d", + "catpart": "3d", + "catproduct": "3d", + "f3d": "3d", + "iam": "3d", + "ige": "3d", + "iges": "3d", + "igs": "3d", + "ipt": "3d", + "jt": "3d", "mqo": "3d", "pmd": "3d", "pmx": "3d", + "prt": "3d", + "sab": "3d", + "sat": "3d", "skp": "3d", + "sldasm": "3d", + "slddrw": "3d", + "sldprt": "3d", + "smb": "3d", + "smt": "3d", "vac": "3d", "vdp": "3d", "vox": "3d", @@ -10109,6 +10370,10 @@ "wrl": "3d", "usd": "3d", "usdz": "3d", + "wire": "3d", + "x_b": "3d", + "x_t": "3d", + "123dx": "3d", "svg": "svg", "ai": "adobe-illustrator", "ait": "adobe-illustrator", @@ -10230,6 +10495,11 @@ "fen": "chess", "gmi": "gemini", "gemini": "gemini", + "php": "php", + "php4": "php", + "php5": "php", + "phtml": "php", + "ctp": "php", "tsconfig.json": "tsconfig", "tauri": "tauri", "jsconfig.json": "jsconfig", @@ -10297,6 +10567,13 @@ "wgsl": "shader", "spv": "shader", "slang": "shader", + "cginc": "shader", + "compute": "shader", + "fx": "shader", + "fxh": "shader", + "hlsli": "shader", + "psh": "shader", + "vsh": "shader", "sy": "siyuan", "ndst.yml": "ndst", "ndst.yaml": "ndst", @@ -10409,65 +10686,22 @@ "lean": "lean", "sls": "salt", "m2": "macaulay2", + "ua": "uiua", "skill.md": "skill", "skills.md": "skill", "instructions.md": "instructions", "instruction.md": "instructions", - "cljx": "clojure", - "clojure": "clojure", - "edn": "clojure", - "ccm": "cpp", - "cxxm": "cpp", - "c++m": "cpp", - "ipp": "cpp", - "ixx": "cpp", - "tpp": "cpp", - "txx": "cpp", - "hpp.in": "cpp", - "h.in": "cpp", - "diff": "diff", - "rej": "diff", - "fsscript": "fsharp", + "mrpack": "mrpack", "gitignore_global": "ignore", "gitignore": "ignore", "git-blame-ignore-revs": "ignore", - "gvy": "groovy", - "nf": "groovy", - "handlebars": "handlebars", - "hjs": "handlebars", - "hlsli": "hlsl", - "fx": "hlsl", - "fxh": "hlsl", - "vsh": "hlsl", - "psh": "hlsl", - "cginc": "hlsl", - "compute": "hlsl", - "html": "html", - "shtml": "html", - "xht": "html", - "aspx": "html", - "jshtm": "html", - "volt": "html", - "rhtml": "html", - "directory": "properties", "gitattributes": "properties", "gitconfig": "properties", "gitmodules": "properties", "editorconfig": "properties", - "repo": "properties", - "jav": "java", - "js": "javascript", - "es6": "javascript", - "cjs": "javascript", - "pac": "javascript", "bowerrc": "json", "jscsrc": "json", - "webmanifest": "json", - "ts.map": "json", - "har": "json", "jslintrc": "json", - "jsonld": "json", - "geojson": "json", "vuerc": "json", "eslintrc": "jsonc", "eslintrc.json": "jsonc", @@ -10476,134 +10710,14 @@ "hintrc": "jsonc", "babelrc": "jsonc", "jmd": "juliamarkdown", - "cls": "tex", - "tex": "latex", - "ltx": "latex", - "mak": "makefile", - "mkd": "markdown", - "mdwn": "markdown", - "mdown": "markdown", - "markdn": "markdown", - "mdtxt": "markdown", - "mdtext": "markdown", - "workbook": "markdown", "npmignore": "ignore", - "npmrc": "properties", - "m": "objective-c", - "mm": "objective-cpp", - "pod": "perl", - "t": "perl", - "psgi": "perl", "rakumod": "raku", "rakutest": "raku", "rakudoc": "raku", "nqp": "raku", "p6": "raku", "pl6": "raku", - "pm6": "raku", - "php": "php", - "php4": "php", - "php5": "php", - "phtml": "php", - "ctp": "php", - "psrc": "powershell", - "rpy": "python", - "pyw": "python", - "cpy": "python", - "gyp": "python", - "gypi": "python", - "pyi": "python", - "ipy": "python", - "pyt": "python", - "rhistory": "r", - "rprofile": "r", - "rt": "r", - "razor": "razor", - "rbx": "ruby", - "rjs": "ruby", - "gemspec": "ruby", - "rake": "ruby", - "ru": "ruby", - "podspec": "ruby", - "rbi": "ruby", - "bashrc": "shellscript", - "bash_aliases": "shellscript", - "bash_profile": "shellscript", - "bash_login": "shellscript", - "ebuild": "shellscript", - "eclass": "shellscript", - "profile": "shellscript", - "bash_logout": "shellscript", - "xprofile": "shellscript", - "xsession": "shellscript", - "xsessionrc": "shellscript", - "zshrc": "shellscript", - "zprofile": "shellscript", - "zlogin": "shellscript", - "zlogout": "shellscript", - "zshenv": "shellscript", - "zsh-theme": "shellscript", - "cshrc": "shellscript", - "tcshrc": "shellscript", - "yashrc": "shellscript", - "yash_profile": "shellscript", - "dsql": "sql", - "ts": "typescript", - "cts": "typescript", - "mts": "typescript", - "brs": "vb", - "bas": "vb", - "vba": "vb", - "ascx": "xml", - "atom": "xml", - "axml": "xml", - "axaml": "xml", - "bpmn": "xml", - "csl": "xml", - "csproj.user": "xml", - "dita": "xml", - "ditamap": "xml", - "ent": "xml", - "dtml": "xml", - "fxml": "xml", - "isml": "xml", - "jmx": "xml", - "launch": "xml", - "menu": "xml", - "opml": "xml", - "owl": "xml", - "proj": "xml", - "publishsettings": "xml", - "pubxml": "xml", - "pubxml.user": "xml", - "rdf": "xml", - "rng": "xml", - "rss": "xml", - "shproj": "xml", - "storyboard": "xml", - "targets": "xml", - "tld": "xml", - "tmx": "xml", - "vbproj": "xml", - "vbproj.user": "xml", - "wsdl": "xml", - "wxi": "xml", - "wxl": "xml", - "wxs": "xml", - "xbl": "xml", - "xib": "xml", - "xliff": "xml", - "xpdl": "xml", - "xul": "xml", - "xoml": "xml", - "yaml": "yaml", - "yml": "yaml", - "eyaml": "yaml", - "eyml": "yaml", - "cff": "yaml", - "yaml-tmpreferences": "yaml", - "yaml-tmtheme": "yaml", - "winget": "yaml" + "pm6": "raku" }, "fileNames": { ".pug-lintrc": "pug", @@ -10637,6 +10751,7 @@ "playwright-ct.config.cts": "playwright", "playwright-ct.config.mts": "playwright", ".htaccess": "xml", + "jakefile": "javascript", ".release-it.json": "rocket", ".release-it.ts": "rocket", ".release-it.js": "rocket", @@ -10713,6 +10828,17 @@ "pre-commit": "console", "pre-push": "console", "post-merge": "console", + ".envrc": "console", + ".hushlogin": "console", + "apkbuild": "console", + "pkgbuild": "console", + "bashrc_apple_terminal": "console", + "zlogin": "console", + "zlogout": "console", + "zprofile": "console", + "zshenv": "console", + "zshrc": "console", + "zshrc_apple_terminal": "console", "excalidraw": "excalidraw", "excalidraw.json": "excalidraw", "excalidraw.svg": "excalidraw", @@ -10759,6 +10885,25 @@ "keystatic.config.jsx": "keystatic", "keystatic.config.js": "keystatic", ".ruby-version": "ruby", + "appraisals": "ruby", + "berksfile": "ruby", + "berksfile.lock": "ruby", + "brewfile": "ruby", + "capfile": "ruby", + "cheffile": "ruby", + "dangerfile": "ruby", + "deliverfile": "ruby", + "guardfile": "ruby", + "gymfile": "ruby", + "hobofile": "ruby", + "matchfile": "ruby", + "podfile": "ruby", + "puppetfile": "ruby", + "rakefile": "ruby", + "rantfile": "ruby", + "scanfile": "ruby", + "snapfile": "ruby", + "thorfile": "ruby", "gemfile": "gemfile", ".rubocop.yml": "rubocop", ".rubocop-todo.yml": "rubocop", @@ -10927,6 +11072,9 @@ ".git-blame-ignore-revs": "git", ".git-for-windows-updater": "git", "git-history": "git", + "commit_editmsg": "git", + "merge_msg": "git", + "git-rebase-todo": "git", ".luacheckrc": "lua", ".rhistory": "r", ".pubignore": "dart", @@ -10936,10 +11084,13 @@ "semgrep.yml": "semgrep", ".semgrepignore": "semgrep", "vue.config.js": "vue-config", + "vue.config.cjs": "vue-config", + "vue.config.mjs": "vue-config", "vue.config.ts": "vue-config", "vetur.config.js": "vue-config", "vetur.config.ts": "vue-config", "volar.config.js": "vue-config", + ".vuerc": "vue-config", "nuxt.config.js": "nuxt", "nuxt.config.ts": "nuxt", ".nuxtignore": "nuxt", @@ -10955,6 +11106,7 @@ "ng-package.json": "angular", ".mjmlconfig": "mjml", "vercel.json": "vercel", + "vercel.ts": "vercel", ".vercelignore": "vercel", "now.json": "vercel", ".nowignore": "vercel", @@ -11715,6 +11867,8 @@ ".browserslistrc": "browserlist", ".snyk": "snyk", ".drone.yml": "drone", + "opencode.json": "opencode", + "opencode.jsonc": "opencode", ".sequelizerc": "sequelize", "gatsby-config.js": "gatsby", "gatsby-config.mjs": "gatsby", @@ -12585,6 +12739,7 @@ "zeabur.yml": "zeabur", "zeabur.toml": "zeabur", "copilot-instructions.md": "copilot", + ".copilotignore": "copilot", ".pre-commit-config.yaml": "pre-commit", ".pre-commit-hooks.yaml": "pre-commit", ".lintstagedrc": "lintstaged", @@ -12666,6 +12821,9 @@ "metro.config.cjs": "metro", "metro.config.mjs": "metro", "metro.config.json": "metro", + "metro.config.ts": "metro", + "metro.config.cts": "metro", + "metro.config.mts": "metro", "src/bashly.yaml": "bashly", "src/bashly.yml": "bashly", "bashly-settings.yaml": "bashly-settings", @@ -12680,6 +12838,14 @@ "skill.md": "skill", "instructions.md": "instructions", "instruction.md": "instructions", + "tsdown.config.ts": "tsdown", + "tsdown.config.mts": "tsdown", + "tsdown.config.cts": "tsdown", + "tsdown.config.js": "tsdown", + "tsdown.config.mjs": "tsdown", + "tsdown.config.cjs": "tsdown", + "tsdown.config.json": "tsdown", + "tsdown.config": "tsdown", "appwrite.json": "appwrite", "appwrite.js": "appwrite", "appwrite.ts": "appwrite", @@ -12934,6 +13100,7 @@ ".rubocop-todo.yml": "rubocop_light", ".rubocop_todo.yml": "rubocop_light", "vercel.json": "vercel_light", + "vercel.ts": "vercel_light", ".vercelignore": "vercel_light", "now.json": "vercel_light", ".nowignore": "vercel_light", @@ -13001,6 +13168,8 @@ "browserslist": "browserlist_light", ".browserslistrc": "browserlist_light", ".drone.yml": "drone_light", + "opencode.json": "opencode_light", + "opencode.jsonc": "opencode_light", ".wakatime-project": "wakatime_light", "circle.yml": "circleci_light", ".releaserc": "semantic-release_light", @@ -13086,6 +13255,7 @@ "zeabur.yml": "zeabur_light", "zeabur.toml": "zeabur_light", "copilot-instructions.md": "copilot_light", + ".copilotignore": "copilot_light", "hosts": "hosts_light", ".cursorignore": "cursor_light", ".cursorindexingignore": "cursor_light", @@ -13129,11 +13299,11 @@ "_idea": "folder-intellij_light", "-idea": "folder-intellij_light", "__idea__": "folder-intellij_light", + "cursor": "folder-cursor_light", ".cursor": "folder-cursor_light", - "..cursor": "folder-cursor_light", - "_.cursor": "folder-cursor_light", - "-.cursor": "folder-cursor_light", - "__.cursor__": "folder-cursor_light" + "_cursor": "folder-cursor_light", + "-cursor": "folder-cursor_light", + "__cursor__": "folder-cursor_light" }, "folderNamesExpanded": { "jinja": "folder-jinja-open_light", @@ -13156,11 +13326,11 @@ "_idea": "folder-intellij-open_light", "-idea": "folder-intellij-open_light", "__idea__": "folder-intellij-open_light", + "cursor": "folder-cursor-open_light", ".cursor": "folder-cursor-open_light", - "..cursor": "folder-cursor-open_light", - "_.cursor": "folder-cursor-open_light", - "-.cursor": "folder-cursor-open_light", - "__.cursor__": "folder-cursor-open_light" + "_cursor": "folder-cursor-open_light", + "-cursor": "folder-cursor-open_light", + "__cursor__": "folder-cursor-open_light" }, "rootFolderNames": {}, "rootFolderNamesExpanded": {} diff --git a/options/fileicon/material-icon-svgs.json b/options/fileicon/material-icon-svgs.json index 5c673ad3d2..a4109d9fa6 100644 --- a/options/fileicon/material-icon-svgs.json +++ b/options/fileicon/material-icon-svgs.json @@ -196,577 +196,583 @@ "firebase": "", "flash": "", "flow": "", - "folder-admin-open": "", - "folder-admin": "", - "folder-android-open": "", - "folder-android": "", - "folder-angular-open": "", - "folder-angular": "", - "folder-animation-open": "", - "folder-animation": "", - "folder-ansible-open": "", - "folder-ansible": "", - "folder-api-open": "", - "folder-api": "", - "folder-apollo-open": "", - "folder-apollo": "", - "folder-app-open": "", - "folder-app": "", + "folder-admin-open": "", + "folder-admin": "", + "folder-android-open": "", + "folder-android": "", + "folder-angular-open": "", + "folder-angular": "", + "folder-animation-open": "", + "folder-animation": "", + "folder-ansible-open": "", + "folder-ansible": "", + "folder-api-open": "", + "folder-api": "", + "folder-apollo-open": "", + "folder-apollo": "", + "folder-app-open": "", + "folder-app": "", "folder-appwrite-open": "", "folder-appwrite": "", - "folder-archive-open": "", - "folder-archive": "", + "folder-archive-open": "", + "folder-archive": "", "folder-assembly-open": "", "folder-assembly": "", - "folder-astro-open": "", - "folder-astro": "", - "folder-atom-open": "", - "folder-atom": "", - "folder-attachment-open": "", - "folder-attachment": "", - "folder-audio-open": "", - "folder-audio": "", - "folder-aurelia-open": "", - "folder-aurelia": "", - "folder-aws-open": "", - "folder-aws": "", - "folder-azure-pipelines-open": "", - "folder-azure-pipelines": "", - "folder-backup-open": "", - "folder-backup": "", - "folder-base-open": "", - "folder-base": "", - "folder-batch-open": "", - "folder-batch": "", - "folder-benchmark-open": "", - "folder-benchmark": "", - "folder-bibliography-open": "", - "folder-bibliography": "", - "folder-bicep-open": "", - "folder-bicep": "", - "folder-blender-open": "", - "folder-blender": "", - "folder-bloc-open": "", - "folder-bloc": "", - "folder-bower-open": "", - "folder-bower": "", - "folder-buildkite-open": "", - "folder-buildkite": "", - "folder-cart-open": "", - "folder-cart": "", - "folder-changesets-open": "", - "folder-changesets": "", - "folder-ci-open": "", - "folder-ci": "", - "folder-circleci-open": "", - "folder-circleci": "", - "folder-class-open": "", - "folder-class": "", - "folder-claude-open": "", - "folder-claude": "", - "folder-client-open": "", - "folder-client": "", - "folder-cline-open": "", - "folder-cline": "", - "folder-cloud-functions-open": "", - "folder-cloud-functions": "", - "folder-cloudflare-open": "", - "folder-cloudflare": "", - "folder-cluster-open": "", - "folder-cluster": "", - "folder-cobol-open": "", - "folder-cobol": "", - "folder-command-open": "", - "folder-command": "", - "folder-components-open": "", - "folder-components": "", - "folder-config-open": "", - "folder-config": "", - "folder-connection-open": "", - "folder-connection": "", - "folder-console-open": "", - "folder-console": "", - "folder-constant-open": "", - "folder-constant": "", - "folder-container-open": "", - "folder-container": "", - "folder-content-open": "", - "folder-content": "", - "folder-context-open": "", - "folder-context": "", - "folder-contract-open": "", - "folder-contract": "", - "folder-controller-open": "", - "folder-controller": "", - "folder-core-open": "", - "folder-core": "", - "folder-coverage-open": "", - "folder-coverage": "", - "folder-css-open": "", - "folder-css": "", - "folder-cue-open": "", - "folder-cue": "", - "folder-cursor-open": "", - "folder-cursor-open_light": "", - "folder-cursor": "", - "folder-cursor_light": "", - "folder-custom-open": "", - "folder-custom": "", - "folder-cypress-open": "", - "folder-cypress": "", - "folder-dal-open": "", - "folder-dal": "", - "folder-dart-open": "", - "folder-dart": "", - "folder-database-open": "", - "folder-database": "", - "folder-debug-open": "", - "folder-debug": "", - "folder-decorators-open": "", - "folder-decorators": "", - "folder-delta-open": "", - "folder-delta": "", - "folder-deprecated-open.clone": "", - "folder-deprecated.clone": "", - "folder-desktop-open": "", - "folder-desktop": "", - "folder-development-open.clone": "", - "folder-development.clone": "", - "folder-directive-open": "", - "folder-directive": "", - "folder-dist-open": "", - "folder-dist": "", - "folder-docker-open": "", - "folder-docker": "", - "folder-docs-open": "", - "folder-docs": "", - "folder-download-open": "", - "folder-download": "", - "folder-drizzle-open": "", - "folder-drizzle": "", - "folder-dump-open": "", - "folder-dump": "", - "folder-eas-open.clone": "", - "folder-eas.clone": "", - "folder-element-open": "", - "folder-element": "", - "folder-enum-open": "", - "folder-enum": "", - "folder-environment-open": "", - "folder-environment": "", - "folder-error-open": "", - "folder-error": "", - "folder-eslint-open": "", - "folder-eslint": "", - "folder-event-open": "", - "folder-event": "", - "folder-examples-open": "", - "folder-examples": "", - "folder-expo-open": "", - "folder-expo": "", - "folder-export-open": "", - "folder-export": "", - "folder-fastlane-open": "", - "folder-fastlane": "", - "folder-favicon-open": "", - "folder-favicon": "", - "folder-features-open": "", - "folder-features": "", + "folder-astro-open": "", + "folder-astro": "", + "folder-atom-open": "", + "folder-atom": "", + "folder-attachment-open": "", + "folder-attachment": "", + "folder-audio-open": "", + "folder-audio": "", + "folder-aurelia-open": "", + "folder-aurelia": "", + "folder-aws-open": "", + "folder-aws": "", + "folder-azure-pipelines-open": "", + "folder-azure-pipelines": "", + "folder-backup-open": "", + "folder-backup": "", + "folder-base-open": "", + "folder-base": "", + "folder-batch-open": "", + "folder-batch": "", + "folder-benchmark-open": "", + "folder-benchmark": "", + "folder-bibliography-open": "", + "folder-bibliography": "", + "folder-bicep-open": "", + "folder-bicep": "", + "folder-blender-open": "", + "folder-blender": "", + "folder-bloc-open": "", + "folder-bloc": "", + "folder-bower-open": "", + "folder-bower": "", + "folder-buildkite-open": "", + "folder-buildkite": "", + "folder-cart-open": "", + "folder-cart": "", + "folder-changesets-open": "", + "folder-changesets": "", + "folder-ci-open": "", + "folder-ci": "", + "folder-circleci-open": "", + "folder-circleci": "", + "folder-class-open": "", + "folder-class": "", + "folder-claude-open": "", + "folder-claude": "", + "folder-client-open": "", + "folder-client": "", + "folder-cline-open": "", + "folder-cline": "", + "folder-cloud-functions-open": "", + "folder-cloud-functions": "", + "folder-cloudflare-open": "", + "folder-cloudflare": "", + "folder-cluster-open": "", + "folder-cluster": "", + "folder-cobol-open": "", + "folder-cobol": "", + "folder-command-open": "", + "folder-command": "", + "folder-components-open": "", + "folder-components": "", + "folder-config-open": "", + "folder-config": "", + "folder-connection-open": "", + "folder-connection": "", + "folder-console-open": "", + "folder-console": "", + "folder-constant-open": "", + "folder-constant": "", + "folder-container-open": "", + "folder-container": "", + "folder-content-open": "", + "folder-content": "", + "folder-context-open": "", + "folder-context": "", + "folder-contract-open": "", + "folder-contract": "", + "folder-controller-open": "", + "folder-controller": "", + "folder-core-open": "", + "folder-core": "", + "folder-coverage-open": "", + "folder-coverage": "", + "folder-css-open": "", + "folder-css": "", + "folder-cue-open": "", + "folder-cue": "", + "folder-cursor-open": "", + "folder-cursor-open_light": "", + "folder-cursor": "", + "folder-cursor_light": "", + "folder-custom-open": "", + "folder-custom": "", + "folder-cypress-open": "", + "folder-cypress": "", + "folder-dal-open": "", + "folder-dal": "", + "folder-dart-open": "", + "folder-dart": "", + "folder-database-open": "", + "folder-database": "", + "folder-debug-open": "", + "folder-debug": "", + "folder-decorators-open": "", + "folder-decorators": "", + "folder-delta-open": "", + "folder-delta": "", + "folder-deprecated-open.clone": "", + "folder-deprecated.clone": "", + "folder-desktop-open": "", + "folder-desktop": "", + "folder-development-open.clone": "", + "folder-development.clone": "", + "folder-directive-open": "", + "folder-directive": "", + "folder-dist-open": "", + "folder-dist": "", + "folder-docker-open": "", + "folder-docker": "", + "folder-docs-open": "", + "folder-docs": "", + "folder-download-open": "", + "folder-download": "", + "folder-drizzle-open": "", + "folder-drizzle": "", + "folder-dump-open": "", + "folder-dump": "", + "folder-eas-open.clone": "", + "folder-eas.clone": "", + "folder-element-open": "", + "folder-element": "", + "folder-enum-open": "", + "folder-enum": "", + "folder-environment-open": "", + "folder-environment": "", + "folder-error-open": "", + "folder-error": "", + "folder-eslint-open": "", + "folder-eslint": "", + "folder-event-open": "", + "folder-event": "", + "folder-examples-open": "", + "folder-examples": "", + "folder-expo-open": "", + "folder-expo": "", + "folder-export-open": "", + "folder-export": "", + "folder-fastlane-open": "", + "folder-fastlane": "", + "folder-favicon-open": "", + "folder-favicon": "", + "folder-features-open": "", + "folder-features": "", "folder-filter-open": "", "folder-filter": "", - "folder-firebase-open": "", - "folder-firebase": "", - "folder-firestore-open": "", - "folder-firestore": "", - "folder-flow-open": "", - "folder-flow": "", - "folder-flutter-open": "", - "folder-flutter": "", - "folder-font-open": "", - "folder-font": "", - "folder-forgejo-open": "", - "folder-forgejo": "", + "folder-firebase-open": "", + "folder-firebase": "", + "folder-firestore-open": "", + "folder-firestore": "", + "folder-flow-open": "", + "folder-flow": "", + "folder-flutter-open": "", + "folder-flutter": "", + "folder-font-open": "", + "folder-font": "", + "folder-forgejo-open": "", + "folder-forgejo": "", "folder-form-open": "", "folder-form": "", - "folder-functions-open": "", - "folder-functions": "", - "folder-gamemaker-open": "", - "folder-gamemaker": "", - "folder-gemini-ai-open": "", - "folder-gemini-ai": "", - "folder-generator-open": "", - "folder-generator": "", - "folder-gh-workflows-open": "", - "folder-gh-workflows": "", - "folder-git-open": "", - "folder-git": "", - "folder-gitea-open": "", - "folder-gitea-workflows-open.clone": "", - "folder-gitea-workflows.clone": "", - "folder-gitea": "", - "folder-github-open": "", - "folder-github": "", - "folder-gitlab-open": "", - "folder-gitlab": "", - "folder-global-open": "", - "folder-global": "", + "folder-functions-open": "", + "folder-functions": "", + "folder-gamemaker-open": "", + "folder-gamemaker": "", + "folder-gemini-ai-open": "", + "folder-gemini-ai": "", + "folder-generator-open": "", + "folder-generator": "", + "folder-gh-workflows-open": "", + "folder-gh-workflows": "", + "folder-git-open": "", + "folder-git": "", + "folder-gitea-open": "", + "folder-gitea-workflows-open.clone": "", + "folder-gitea-workflows.clone": "", + "folder-gitea": "", + "folder-github-open": "", + "folder-github": "", + "folder-gitlab-open": "", + "folder-gitlab": "", + "folder-global-open": "", + "folder-global": "", "folder-go-open": "", "folder-go": "", - "folder-godot-open": "", - "folder-godot": "", - "folder-gradle-open": "", - "folder-gradle": "", - "folder-graphql-open": "", - "folder-graphql": "", - "folder-guard-open": "", - "folder-guard": "", - "folder-gulp-open": "", - "folder-gulp": "", - "folder-helm-open": "", - "folder-helm": "", - "folder-helper-open": "", - "folder-helper": "", - "folder-home-open": "", - "folder-home": "", - "folder-hook-open": "", - "folder-hook": "", - "folder-husky-open": "", - "folder-husky": "", - "folder-i18n-open": "", - "folder-i18n": "", - "folder-images-open": "", - "folder-images": "", - "folder-import-open": "", - "folder-import": "", - "folder-include-open": "", - "folder-include": "", - "folder-input-open": "", - "folder-input": "", - "folder-instructions-open.clone": "", - "folder-instructions.clone": "", - "folder-intellij-open": "", - "folder-intellij-open_light": "", - "folder-intellij": "", - "folder-intellij_light": "", - "folder-interceptor-open": "", - "folder-interceptor": "", - "folder-interface-open": "", - "folder-interface": "", - "folder-ios-open": "", - "folder-ios": "", - "folder-java-open": "", - "folder-java": "", - "folder-javascript-open": "", - "folder-javascript": "", - "folder-jinja-open": "", - "folder-jinja-open_light": "", - "folder-jinja": "", - "folder-jinja_light": "", - "folder-job-open": "", - "folder-job": "", - "folder-json-open": "", - "folder-json": "", - "folder-jupyter-open": "", - "folder-jupyter": "", - "folder-keys-open": "", - "folder-keys": "", + "folder-godot-open": "", + "folder-godot": "", + "folder-gradle-open": "", + "folder-gradle": "", + "folder-graphql-open": "", + "folder-graphql": "", + "folder-guard-open": "", + "folder-guard": "", + "folder-gulp-open": "", + "folder-gulp": "", + "folder-helm-open": "", + "folder-helm": "", + "folder-helper-open": "", + "folder-helper": "", + "folder-home-open": "", + "folder-home": "", + "folder-hook-open": "", + "folder-hook": "", + "folder-husky-open": "", + "folder-husky": "", + "folder-i18n-open": "", + "folder-i18n": "", + "folder-images-open": "", + "folder-images": "", + "folder-import-open": "", + "folder-import": "", + "folder-include-open": "", + "folder-include": "", + "folder-input-open": "", + "folder-input": "", + "folder-instructions-open.clone": "", + "folder-instructions.clone": "", + "folder-intellij-open": "", + "folder-intellij-open_light": "", + "folder-intellij": "", + "folder-intellij_light": "", + "folder-interceptor-open": "", + "folder-interceptor": "", + "folder-interface-open": "", + "folder-interface": "", + "folder-ios-open": "", + "folder-ios": "", + "folder-java-open": "", + "folder-java": "", + "folder-javascript-open": "", + "folder-javascript": "", + "folder-jinja-open": "", + "folder-jinja-open_light": "", + "folder-jinja": "", + "folder-jinja_light": "", + "folder-job-open": "", + "folder-job": "", + "folder-json-open": "", + "folder-json": "", + "folder-jupyter-open": "", + "folder-jupyter": "", + "folder-keys-open": "", + "folder-keys": "", "folder-kotlin-open": "", "folder-kotlin": "", - "folder-kubernetes-open": "", - "folder-kubernetes": "", - "folder-kusto-open": "", - "folder-kusto": "", - "folder-layout-open": "", - "folder-layout": "", - "folder-lefthook-open": "", - "folder-lefthook": "", - "folder-less-open": "", - "folder-less": "", - "folder-lib-open": "", - "folder-lib": "", - "folder-license-open": "", - "folder-license": "", - "folder-link-open": "", - "folder-link": "", - "folder-linux-open": "", - "folder-linux": "", - "folder-liquibase-open": "", - "folder-liquibase": "", - "folder-log-open": "", - "folder-log": "", - "folder-lottie-open": "", - "folder-lottie": "", - "folder-lua-open": "", - "folder-lua": "", - "folder-luau-open": "", - "folder-luau": "", - "folder-macos-open": "", - "folder-macos": "", - "folder-mail-open": "", - "folder-mail": "", - "folder-mappings-open": "", - "folder-mappings": "", - "folder-markdown-open": "", - "folder-markdown": "", - "folder-mercurial-open": "", - "folder-mercurial": "", - "folder-messages-open": "", - "folder-messages": "", - "folder-meta-open": "", - "folder-meta": "", - "folder-metro-open": "", - "folder-metro": "", - "folder-middleware-open": "", - "folder-middleware": "", - "folder-migrations-open": "", - "folder-migrations": "", - "folder-mjml-open": "", - "folder-mjml": "", - "folder-mobile-open": "", - "folder-mobile": "", - "folder-mock-open": "", - "folder-mock": "", - "folder-mojo-open": "", - "folder-mojo": "", - "folder-molecule-open": "", - "folder-molecule": "", - "folder-moon-open": "", - "folder-moon": "", - "folder-netlify-open": "", - "folder-netlify": "", - "folder-next-open": "", - "folder-next": "", - "folder-nginx-open": "", - "folder-nginx": "", - "folder-ngrx-actions-open.clone": "", - "folder-ngrx-actions.clone": "", - "folder-ngrx-effects-open.clone": "", - "folder-ngrx-effects.clone": "", - "folder-ngrx-entities-open.clone": "", - "folder-ngrx-entities.clone": "", - "folder-ngrx-reducer-open.clone": "", - "folder-ngrx-reducer.clone": "", - "folder-ngrx-selectors-open.clone": "", - "folder-ngrx-selectors.clone": "", - "folder-ngrx-state-open.clone": "", - "folder-ngrx-state.clone": "", - "folder-ngrx-store-open": "", - "folder-ngrx-store": "", - "folder-node-open": "", - "folder-node": "", - "folder-nuxt-open": "", - "folder-nuxt": "", - "folder-obsidian-open": "", - "folder-obsidian": "", + "folder-kubernetes-open": "", + "folder-kubernetes": "", + "folder-kusto-open": "", + "folder-kusto": "", + "folder-layout-open": "", + "folder-layout": "", + "folder-lefthook-open": "", + "folder-lefthook": "", + "folder-less-open": "", + "folder-less": "", + "folder-lib-open": "", + "folder-lib": "", + "folder-license-open": "", + "folder-license": "", + "folder-link-open": "", + "folder-link": "", + "folder-linux-open": "", + "folder-linux": "", + "folder-liquibase-open": "", + "folder-liquibase": "", + "folder-log-open": "", + "folder-log": "", + "folder-lottie-open": "", + "folder-lottie": "", + "folder-lua-open": "", + "folder-lua": "", + "folder-luau-open": "", + "folder-luau": "", + "folder-macos-open": "", + "folder-macos": "", + "folder-mail-open": "", + "folder-mail": "", + "folder-mappings-open": "", + "folder-mappings": "", + "folder-markdown-open": "", + "folder-markdown": "", + "folder-mercurial-open": "", + "folder-mercurial": "", + "folder-messages-open": "", + "folder-messages": "", + "folder-meta-open": "", + "folder-meta": "", + "folder-metro-open": "", + "folder-metro": "", + "folder-middleware-open": "", + "folder-middleware": "", + "folder-migrations-open": "", + "folder-migrations": "", + "folder-mjml-open": "", + "folder-mjml": "", + "folder-mobile-open": "", + "folder-mobile": "", + "folder-mock-open": "", + "folder-mock": "", + "folder-mojo-open": "", + "folder-mojo": "", + "folder-molecule-open": "", + "folder-molecule": "", + "folder-moon-open": "", + "folder-moon": "", + "folder-netlify-open": "", + "folder-netlify": "", + "folder-next-open": "", + "folder-next": "", + "folder-nginx-open": "", + "folder-nginx": "", + "folder-ngrx-actions-open.clone": "", + "folder-ngrx-actions.clone": "", + "folder-ngrx-effects-open.clone": "", + "folder-ngrx-effects.clone": "", + "folder-ngrx-entities-open.clone": "", + "folder-ngrx-entities.clone": "", + "folder-ngrx-reducer-open.clone": "", + "folder-ngrx-reducer.clone": "", + "folder-ngrx-selectors-open.clone": "", + "folder-ngrx-selectors.clone": "", + "folder-ngrx-state-open.clone": "", + "folder-ngrx-state.clone": "", + "folder-ngrx-store-open": "", + "folder-ngrx-store": "", + "folder-node-open": "", + "folder-node": "", + "folder-nuxt-open": "", + "folder-nuxt": "", + "folder-obsidian-open": "", + "folder-obsidian": "", "folder-open": "", - "folder-organism-open": "", - "folder-organism": "", - "folder-other-open": "", - "folder-other": "", - "folder-packages-open": "", - "folder-packages": "", - "folder-pdf-open": "", - "folder-pdf": "", - "folder-pdm-open": "", - "folder-pdm": "", - "folder-php-open": "", - "folder-php": "", - "folder-phpmailer-open": "", - "folder-phpmailer": "", - "folder-pipe-open": "", - "folder-pipe": "", - "folder-plastic-open": "", - "folder-plastic": "", - "folder-plugin-open": "", - "folder-plugin": "", - "folder-policy-open": "", - "folder-policy": "", - "folder-postman-open": "", - "folder-postman": "", - "folder-powershell-open": "", - "folder-powershell": "", - "folder-prisma-open": "", - "folder-prisma": "", - "folder-private-open": "", - "folder-private": "", - "folder-project-open": "", - "folder-project": "", - "folder-prompts-open": "", - "folder-prompts": "", - "folder-proto-open": "", - "folder-proto": "", - "folder-public-open": "", - "folder-public": "", - "folder-python-open": "", - "folder-python": "", - "folder-pytorch-open": "", - "folder-pytorch": "", - "folder-quasar-open": "", - "folder-quasar": "", - "folder-queue-open": "", - "folder-queue": "", - "folder-r-open": "", - "folder-r": "", - "folder-react-components-open": "", + "folder-opencode-open": "", + "folder-opencode": "", + "folder-organism-open": "", + "folder-organism": "", + "folder-other-open": "", + "folder-other": "", + "folder-packages-open": "", + "folder-packages": "", + "folder-pdf-open": "", + "folder-pdf": "", + "folder-pdm-open": "", + "folder-pdm": "", + "folder-php-open": "", + "folder-php": "", + "folder-phpmailer-open": "", + "folder-phpmailer": "", + "folder-pipe-open": "", + "folder-pipe": "", + "folder-plastic-open": "", + "folder-plastic": "", + "folder-plugin-open": "", + "folder-plugin": "", + "folder-policy-open": "", + "folder-policy": "", + "folder-postman-open": "", + "folder-postman": "", + "folder-powershell-open": "", + "folder-powershell": "", + "folder-prisma-open": "", + "folder-prisma": "", + "folder-private-open": "", + "folder-private": "", + "folder-project-open": "", + "folder-project": "", + "folder-prompts-open": "", + "folder-prompts": "", + "folder-proto-open": "", + "folder-proto": "", + "folder-public-open": "", + "folder-public": "", + "folder-python-open": "", + "folder-python": "", + "folder-pytorch-open": "", + "folder-pytorch": "", + "folder-quasar-open": "", + "folder-quasar": "", + "folder-queue-open": "", + "folder-queue": "", + "folder-r-open": "", + "folder-r": "", + "folder-react-components-open": "", "folder-react-components": "", - "folder-redux-actions-open.clone": "", - "folder-redux-actions.clone": "", - "folder-redux-reducer-open": "", - "folder-redux-reducer": "", - "folder-redux-selector-open.clone": "", - "folder-redux-selector.clone": "", - "folder-redux-store-open.clone": "", - "folder-redux-store.clone": "", - "folder-redux-toolkit-open.clone": "", - "folder-redux-toolkit.clone": "", - "folder-repository-open": "", - "folder-repository": "", - "folder-resolver-open": "", - "folder-resolver": "", - "folder-resource-open": "", - "folder-resource": "", - "folder-review-open": "", - "folder-review": "", - "folder-robot-open": "", - "folder-robot": "", + "folder-redis-open.clone": "", + "folder-redis.clone": "", + "folder-redux-actions-open.clone": "", + "folder-redux-actions.clone": "", + "folder-redux-reducer-open": "", + "folder-redux-reducer": "", + "folder-redux-selector-open.clone": "", + "folder-redux-selector.clone": "", + "folder-redux-store-open.clone": "", + "folder-redux-store.clone": "", + "folder-redux-toolkit-open.clone": "", + "folder-redux-toolkit.clone": "", + "folder-repository-open": "", + "folder-repository": "", + "folder-resolver-open": "", + "folder-resolver": "", + "folder-resource-open": "", + "folder-resource": "", + "folder-review-open": "", + "folder-review": "", + "folder-robot-open": "", + "folder-robot": "", "folder-root-open": "", "folder-root": "", - "folder-routes-open": "", - "folder-routes": "", - "folder-rules-open": "", - "folder-rules": "", - "folder-rust-open": "", - "folder-rust": "", - "folder-salt-open": "", - "folder-salt": "", - "folder-sandbox-open": "", - "folder-sandbox": "", - "folder-sass-open": "", - "folder-sass": "", - "folder-scala-open": "", - "folder-scala": "", - "folder-scons-open": "", - "folder-scons": "", - "folder-scrap-open.clone": "", - "folder-scrap.clone": "", - "folder-scripts-open": "", - "folder-scripts": "", - "folder-secure-open": "", - "folder-secure": "", - "folder-seeders-open": "", - "folder-seeders": "", - "folder-server-open": "", - "folder-server": "", - "folder-serverless-open": "", - "folder-serverless": "", - "folder-shader-open": "", - "folder-shader": "", - "folder-shared-open": "", - "folder-shared": "", - "folder-simulations-open": "", - "folder-simulations": "", - "folder-skills-open": "", - "folder-skills": "", - "folder-snapcraft-open": "", + "folder-routes-open": "", + "folder-routes": "", + "folder-rules-open": "", + "folder-rules": "", + "folder-rust-open": "", + "folder-rust": "", + "folder-salt-open": "", + "folder-salt": "", + "folder-sandbox-open": "", + "folder-sandbox": "", + "folder-sass-open": "", + "folder-sass": "", + "folder-scala-open": "", + "folder-scala": "", + "folder-scons-open": "", + "folder-scons": "", + "folder-scrap-open.clone": "", + "folder-scrap.clone": "", + "folder-scripts-open": "", + "folder-scripts": "", + "folder-secure-open": "", + "folder-secure": "", + "folder-seeders-open": "", + "folder-seeders": "", + "folder-server-open": "", + "folder-server": "", + "folder-serverless-open": "", + "folder-serverless": "", + "folder-shader-open": "", + "folder-shader": "", + "folder-shared-open": "", + "folder-shared": "", + "folder-simulations-open": "", + "folder-simulations": "", + "folder-skills-open": "", + "folder-skills": "", + "folder-snapcraft-open": "", "folder-snapcraft": "", - "folder-snippet-open": "", - "folder-snippet": "", - "folder-src-open": "", - "folder-src-tauri-open": "", - "folder-src-tauri": "", - "folder-src": "", - "folder-stack-open": "", - "folder-stack": "", - "folder-stencil-open": "", - "folder-stencil": "", - "folder-store-open": "", - "folder-store": "", - "folder-storybook-open": "", - "folder-storybook": "", - "folder-stylus-open": "", - "folder-stylus": "", - "folder-sublime-open": "", - "folder-sublime": "", - "folder-supabase-open": "", - "folder-supabase": "", - "folder-svelte-open": "", - "folder-svelte": "", - "folder-svg-open": "", - "folder-svg": "", - "folder-syntax-open": "", - "folder-syntax": "", - "folder-target-open": "", - "folder-target": "", - "folder-taskfile-open": "", - "folder-taskfile": "", - "folder-tasks-open": "", - "folder-tasks": "", - "folder-television-open": "", - "folder-television": "", - "folder-temp-open": "", - "folder-temp": "", - "folder-template-open": "", - "folder-template": "", - "folder-terraform-open": "", - "folder-terraform": "", - "folder-test-open": "", - "folder-test": "", - "folder-theme-open": "", - "folder-theme": "", - "folder-toc-open": "", - "folder-toc": "", - "folder-tools-open": "", - "folder-tools": "", - "folder-trash-open": "", - "folder-trash": "", - "folder-trigger-open": "", - "folder-trigger": "", - "folder-turborepo-open": "", - "folder-turborepo": "", - "folder-typescript-open": "", - "folder-typescript": "", - "folder-ui-open": "", - "folder-ui": "", - "folder-unity-open": "", - "folder-unity": "", - "folder-update-open": "", - "folder-update": "", - "folder-upload-open": "", - "folder-upload": "", - "folder-utils-open": "", - "folder-utils": "", - "folder-vercel-open": "", - "folder-vercel": "", - "folder-verdaccio-open": "", - "folder-verdaccio": "", - "folder-video-open": "", - "folder-video": "", - "folder-views-open": "", - "folder-views": "", - "folder-vm-open": "", - "folder-vm": "", - "folder-vscode-open": "", - "folder-vscode": "", - "folder-vue-directives-open": "", - "folder-vue-directives": "", - "folder-vue-open": "", - "folder-vue": "", - "folder-vuepress-open": "", - "folder-vuepress": "", - "folder-vuex-store-open": "", - "folder-vuex-store": "", - "folder-wakatime-open": "", - "folder-wakatime": "", - "folder-webpack-open": "", - "folder-webpack": "", - "folder-windows-open": "", - "folder-windows": "", - "folder-wordpress-open": "", - "folder-wordpress": "", - "folder-yarn-open": "", - "folder-yarn": "", - "folder-zeabur-open": "", - "folder-zeabur": "", + "folder-snippet-open": "", + "folder-snippet": "", + "folder-src-open": "", + "folder-src-tauri-open": "", + "folder-src-tauri": "", + "folder-src": "", + "folder-stack-open": "", + "folder-stack": "", + "folder-stencil-open": "", + "folder-stencil": "", + "folder-store-open": "", + "folder-store": "", + "folder-storybook-open": "", + "folder-storybook": "", + "folder-stylus-open": "", + "folder-stylus": "", + "folder-sublime-open": "", + "folder-sublime": "", + "folder-supabase-open": "", + "folder-supabase": "", + "folder-svelte-open": "", + "folder-svelte": "", + "folder-svg-open": "", + "folder-svg": "", + "folder-syntax-open": "", + "folder-syntax": "", + "folder-target-open": "", + "folder-target": "", + "folder-taskfile-open": "", + "folder-taskfile": "", + "folder-tasks-open": "", + "folder-tasks": "", + "folder-television-open": "", + "folder-television": "", + "folder-temp-open": "", + "folder-temp": "", + "folder-template-open": "", + "folder-template": "", + "folder-terraform-open": "", + "folder-terraform": "", + "folder-test-open": "", + "folder-test": "", + "folder-theme-open": "", + "folder-theme": "", + "folder-toc-open": "", + "folder-toc": "", + "folder-tools-open": "", + "folder-tools": "", + "folder-trash-open": "", + "folder-trash": "", + "folder-trigger-open": "", + "folder-trigger": "", + "folder-turborepo-open": "", + "folder-turborepo": "", + "folder-typescript-open": "", + "folder-typescript": "", + "folder-ui-open": "", + "folder-ui": "", + "folder-unity-open": "", + "folder-unity": "", + "folder-update-open": "", + "folder-update": "", + "folder-upload-open": "", + "folder-upload": "", + "folder-utils-open": "", + "folder-utils": "", + "folder-vercel-open": "", + "folder-vercel": "", + "folder-verdaccio-open": "", + "folder-verdaccio": "", + "folder-video-open": "", + "folder-video": "", + "folder-views-open": "", + "folder-views": "", + "folder-vm-open": "", + "folder-vm": "", + "folder-vscode-open": "", + "folder-vscode": "", + "folder-vue-directives-open": "", + "folder-vue-directives": "", + "folder-vue-open": "", + "folder-vue": "", + "folder-vuepress-open": "", + "folder-vuepress": "", + "folder-vuex-store-open": "", + "folder-vuex-store": "", + "folder-wakatime-open": "", + "folder-wakatime": "", + "folder-webpack-open": "", + "folder-webpack": "", + "folder-windows-open": "", + "folder-windows": "", + "folder-wordpress-open": "", + "folder-wordpress": "", + "folder-yarn-open": "", + "folder-yarn": "", + "folder-zeabur-open": "", + "folder-zeabur": "", + "folder-zed-open": "", + "folder-zed": "", "folder": "", "font": "", "forth": "", @@ -819,6 +825,7 @@ "helm": "", "heroku": "", "hex": "", + "hip": "", "histoire": "", "hjson": "", "horusec": "", @@ -922,6 +929,7 @@ "mojo": "", "moon": "", "moonscript": "", + "mrpack": "", "mxml": "", "nano-staged": "", "nano-staged_light": "", @@ -968,6 +976,8 @@ "opam": "", "openapi": "", "openapi_light": "", + "opencode": "", + "opencode_light": "", "opentofu": "", "opentofu_light": "", "otne": "", @@ -1166,6 +1176,7 @@ "trigger": "", "tsconfig": "", "tsdoc": "", + "tsdown": "", "tsil": "", "tune": "", "turborepo": "", @@ -1176,6 +1187,7 @@ "typescript-def": "", "typescript": "", "typst": "", + "uiua": "", "umi": "", "uml": "", "uml_light": "", diff --git a/options/locale/locale_cs-CZ.json b/options/locale/locale_cs-CZ.json index 52e9940db1..081a540587 100644 --- a/options/locale/locale_cs-CZ.json +++ b/options/locale/locale_cs-CZ.json @@ -181,7 +181,6 @@ "startpage.platform_desc": "Gitea běží všude, kde Go může kompilovat: Windows, macOS, Linux, ARM, atd. Vyberte si ten, který milujete!", "startpage.lightweight": "Lehká", "startpage.lightweight_desc": "Gitea má minimální požadavky a může běžet na Raspberry Pi. Šetřete energii vašeho stroje!", - "startpage.license_desc": "Vše je na %[2]s! Připojte se tím, že přispějete a uděláte tento projekt ještě lepší. Nestyďte se být přispěvatel!", "install.install": "Instalace", "install.title": "Výchozí konfigurace", "install.docker_helper": "Pokud spouštíte Gitea v Dockeru, přečtěte si dokumentaci, než budete měnit jakákoliv nastavení.", @@ -209,7 +208,6 @@ "install.lfs_path": "Kořenový adresář Git LFS", "install.lfs_path_helper": "V tomto adresáři budou uloženy soubory, které jsou sledovány Git LFS. Pokud ponecháte prázdné, LFS zakážete.", "install.run_user": "Spustit jako uživatel", - "install.run_user_helper": "Zadejte uživatelské jméno, pod kterým Gitea běží v operačním systému. Pozor: tento uživatel musí mít přístup ke kořenovému adresáři repozitářů.", "install.domain": "Doména serveru", "install.domain_helper": "Adresa domény, nebo hostitele serveru.", "install.ssh_port": "Port SSH serveru", @@ -231,12 +229,6 @@ "install.register_confirm": "Pro registraci vyžadovat potvrzení e-mailu", "install.mail_notify": "Povolit e-mailová oznámení", "install.server_service_title": "Nastavení serveru a dalších služeb", - "install.offline_mode": "Povolit místní režim", - "install.offline_mode_popup": "Zakázat sítě pro doručování obsahu a poskytovat veškerý obsah lokálně.", - "install.disable_gravatar": "Zakázat Gravatar", - "install.disable_gravatar_popup": "Zakážete Gravatar a jiné cizí zdroje avatarů. Pokud uživatel nenahraje avatar, bude použit výchozí.", - "install.federated_avatar_lookup": "Povolit avatary z veřejných zdrojů", - "install.federated_avatar_lookup_popup": "Povolte vyhledání avatarů z veřejných zdrojů pro využití služeb založených na libravatar.", "install.disable_registration": "Vypnout možnost uživatelské registrace", "install.disable_registration_popup": "Vypnout možnost registrace. Pouze správci budou moci vytvářet účty.", "install.allow_only_external_registration_popup": "Povolit registraci pouze prostřednictvím externích služeb", @@ -256,12 +248,10 @@ "install.admin_email": "E-mailová adresa", "install.install_btn_confirm": "Nainstalovat Gitea", "install.test_git_failed": "Chyba při testu příkazu 'git': %v", - "install.sqlite3_not_available": "Tato verze Gitea nepodporuje SQLite3. Stáhněte si oficiální binární verzi od %s (nikoli verzi „gobuild“).", "install.invalid_db_setting": "Nastavení databáze je neplatné: %v", "install.invalid_db_table": "Databázová tabulka „%s“ je neplatná: %v", "install.invalid_repo_path": "Kořenový adresář repozitářů není správný: %v", "install.invalid_app_data_path": "Cesta k datům aplikace je neplatná: %v", - "install.run_user_not_match": "\"Run as\" uživatelské jméno není aktuální uživatelské jméno: %s -> %s", "install.internal_token_failed": "Nepodařilo se vytvořit interní token: %v", "install.secret_key_failed": "Nepodařilo se vytvořit tajný klíč: %v", "install.save_config_failed": "Uložení konfigurace se nezdařilo: %v", @@ -541,13 +531,6 @@ "user.block.unblock.failure": "Nepodařilo se odblokovat uživatele: %s", "user.block.blocked": "Zablokovali jste tohoto uživatele.", "user.block.title": "Zablokovat Uživatele", - "user.block.info": "Blokování uživatele brání v interakci s repozitáři, jako je otevírání nebo komentování pull requestů nebo úkolů. Další informace o blokování uživatele.", - "user.block.info_1": "Zablokování uživatele zabrání následujícím akcím na vašem účtu a repozirářích:", - "user.block.info_2": "sledují váš účet", - "user.block.info_3": "pošle vám oznámení pomocí @zmínění vašeho uživatelského jména", - "user.block.info_4": "pozváním vás jako spolupracovníka do jejich repozitářů", - "user.block.info_5": "oblíbení, rozštěpení nebo sledování repozitářů", - "user.block.info_6": "otevření a komentování úkolů nebo pull requestů", "user.block.user_to_block": "Uživatel k blokování", "user.block.note": "Poznámka", "user.block.note.title": "Volitelná poznámka:", @@ -756,7 +739,6 @@ "settings.permissions_list": "Oprávnění:", "settings.manage_oauth2_applications": "Spravovat OAuth2 aplikace", "settings.edit_oauth2_application": "Upravit OAuth2 aplikaci", - "settings.oauth2_applications_desc": "OAuth2 aplikace umožní aplikacím třetích stran bezpečně ověřit uživatele v této instanci Gitea.", "settings.remove_oauth2_application": "Odstranit OAuth2 aplikaci", "settings.remove_oauth2_application_desc": "Odstraněním OAuth2 aplikace odeberete přístup všem podepsaným přístupovým tokenům. Pokračovat?", "settings.remove_oauth2_application_success": "Aplikace byla odstraněna.", @@ -774,7 +756,6 @@ "settings.oauth2_regenerate_secret_hint": "Ztratili jste svůj tajný klíč?", "settings.oauth2_client_secret_hint": "Tajný klíč se znovu nezobrazí po opuštění nebo obnovení této stránky. Ujistěte se, že jste si jej uložili.", "settings.oauth2_application_edit": "Upravit", - "settings.oauth2_application_create_description": "OAuth2 aplikace poskytuje přístup aplikacím třetích stran k uživatelským účtům na této instanci.", "settings.oauth2_application_remove_description": "Odebráním OAuth2 aplikace zabrání přístupu ověřeným uživatelům na této instanci. Pokračovat?", "settings.oauth2_application_locked": "Gitea předregistruje některé OAuth2 aplikace při spuštění, pokud je to povoleno v konfiguraci. Aby se zabránilo neočekávanému chování, nelze je upravovat ani odstranit. Více informací naleznete v dokumentaci OAuth2.", "settings.authorized_oauth2_applications": "Autorizovat OAuth2 aplikaci", @@ -845,7 +826,6 @@ "repo.visibility_description": "Pouze majitelé nebo členové organizace to budou moci vidět, pokud mají práva.", "repo.visibility_helper": "Nastavit repozitář jako soukromý", "repo.visibility_helper_forced": "Váš administrátor vynutil, že nové repozitáře budou soukromé.", - "repo.visibility_fork_helper": "(Změna tohoto ovlivní všechny rozštěpení repozitáře.)", "repo.clone_helper": "Potřebujete pomoci s klonováním? Navštivte nápovědu.", "repo.fork_repo": "Rozštěpení repozitáře", "repo.fork_from": "Rozštěpit z", @@ -931,8 +911,6 @@ "repo.transfer.accept_desc": "Převést do „%s“", "repo.transfer.reject": "Odmítnout převod", "repo.transfer.reject_desc": "Zrušit převod do „%s“", - "repo.transfer.no_permission_to_accept": "Nemáte oprávnění k přijetí tohoto převodu.", - "repo.transfer.no_permission_to_reject": "Nemáte oprávnění k odmítnutí tohoto převodu.", "repo.desc.private": "Soukromý", "repo.desc.public": "Veřejný", "repo.desc.template": "Šablona", @@ -1065,7 +1043,6 @@ "repo.ambiguous_runes_description": "Tento soubor obsahuje znaky Unicode, které mohou být zaměněny s jinými znaky. Pokud si myslíte, že je to záměrné, můžete toto varování bezpečně ignorovat. Použijte tlačítko Escape sekvence k jejich zobrazení.", "repo.invisible_runes_line": "Tento řádek má neviditelné znaky Unicode", "repo.ambiguous_runes_line": "Tento řádek má nejednoznačné znaky Unicode", - "repo.ambiguous_character": "%[1]c [U+%04[1]X] je zaměnitelný s %[2]c [U+%04[2]X]", "repo.escape_control_characters": "Escape sekvence", "repo.unescape_control_characters": "Bez escape sekvencí", "repo.file_copy_permalink": "Kopírovat trvalý odkaz", @@ -1145,7 +1122,6 @@ "repo.editor.upload_file_is_locked": "Soubor „%s“ je uzamčen uživatelem %s.", "repo.editor.upload_files_to_dir": "Nahrát soubory do „%s“", "repo.editor.cannot_commit_to_protected_branch": "Nelze vytvořit commit v chráněné větvi „%s“.", - "repo.editor.no_commit_to_branch": "Nelze odevzdat přímo do větve, protože:", "repo.editor.user_no_push_to_branch": "Uživatel nemůže nahrávat do větve", "repo.editor.require_signed_commit": "Větev vyžaduje podepsaný commit", "repo.editor.cherry_pick": "Cherry-pick %s na:", @@ -1153,7 +1129,6 @@ "repo.commits.desc": "Procházet historii změn zdrojového kódu.", "repo.commits.commits": "Commity", "repo.commits.no_commits": "Žádné společné commity. „%s“ a „%s“ mají zcela odlišnou historii.", - "repo.commits.nothing_to_compare": "Tyto větve jsou stejné.", "repo.commits.search.tooltip": "Můžete předřadit klíčová slova s „author:“, „committer:“, „after:“ nebo „before:“, např. „revert author:Alice before:2019-01-03“.", "repo.commits.search_branch": "Tato větev", "repo.commits.search_all": "Všechny větve", @@ -1224,11 +1199,9 @@ "repo.issues.new": "Nový úkol", "repo.issues.new.title_empty": "Název nesmí být prázdný", "repo.issues.new.labels": "Štítky", - "repo.issues.new.no_label": "Bez štítku", "repo.issues.new.clear_labels": "Zrušit štítky", "repo.issues.new.projects": "Projekty", "repo.issues.new.clear_projects": "Vymazat projekty", - "repo.issues.new.no_projects": "Žádný projekt", "repo.issues.new.open_projects": "Otevřít projekty", "repo.issues.new.closed_projects": "Uzavřené projekty", "repo.issues.new.no_items": "Žádné položky", @@ -1351,7 +1324,6 @@ "repo.issues.comment_pull_merged_at": "sloučený commit %[1]s do %[2]s %[3]s", "repo.issues.comment_manually_pull_merged_at": "ručně sloučený commit %[1]s do %[2]s %[3]s", "repo.issues.close_comment_issue": "Okomentovat a zavřít", - "repo.issues.reopen_issue": "Znovuotevřít", "repo.issues.reopen_comment_issue": "Znovu otevřít s komentářem", "repo.issues.create_comment": "Okomentovat", "repo.issues.comment.blocked_user": "Nemůžete vytvořit nebo upravovat komentář, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře.", @@ -1557,7 +1529,6 @@ "repo.pulls.allow_edits_from_maintainers": "Povolit úpravy od správců", "repo.pulls.allow_edits_from_maintainers_desc": "Uživatelé s přístupem k zápisu do základní větve mohou také nahrávat do této větve", "repo.pulls.allow_edits_from_maintainers_err": "Aktualizace se nezdařila", - "repo.pulls.compare_changes_desc": "Vyberte větev pro sloučení a větev pro natažení.", "repo.pulls.has_viewed_file": "Zobrazeno", "repo.pulls.has_changed_since_last_review": "Změněno od vašeho posledního posouzení", "repo.pulls.viewed_files_label": "%[1]d / %[2]d souborů zobrazeno", @@ -1574,7 +1545,6 @@ "repo.pulls.showing_specified_commit_range": "Zobrazují se pouze změny mezi %[1]s..%[2]s", "repo.pulls.review_only_possible_for_full_diff": "Posouzení je možné pouze při zobrazení plného rozlišení", "repo.pulls.filter_changes_by_commit": "Filtrovat podle commitu", - "repo.pulls.nothing_to_compare": "Tyto větve jsou stejné. Není potřeba vytvářet pull request.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "Tyto větve jsou stejné. Tento pull request bude prázdný.", "repo.pulls.has_pull_request": "Pull request mezi těmito větvemi již existuje: %[2]s#%[3]d", "repo.pulls.create": "Vytvořit pull request", @@ -1641,7 +1611,6 @@ "repo.pulls.status_checking": "Některé kontroly jsou nedořešeny", "repo.pulls.status_checks_success": "Všechny kontroly byly úspěšné", "repo.pulls.status_checks_warning": "Některé kontroly nahlásily varování", - "repo.pulls.status_checks_failure": "Některé kontroly se nezdařily", "repo.pulls.status_checks_error": "Některé kontroly nahlásily chyby", "repo.pulls.status_checks_requested": "Požadováno", "repo.pulls.status_checks_details": "Podrobnosti", @@ -1719,7 +1688,6 @@ "repo.signing.wont_sign.headsigned": "Sloučení nebude podepsáno, protože hlavní revize není podepsána.", "repo.signing.wont_sign.commitssigned": "Sloučení nebude podepsáno, protože všechny přidružené revize nejsou podepsány.", "repo.signing.wont_sign.approved": "Sloučení nebude podepsáno, protože pull request není schválen.", - "repo.signing.wont_sign.not_signed_in": "Nejste přihlášeni.", "repo.ext_wiki": "Přístup k externí Wiki", "repo.ext_wiki.desc": "Odkaz do externí Wiki.", "repo.wiki.welcome": "Vítejte ve Wiki.", @@ -1928,7 +1896,6 @@ "repo.settings.transfer_abort_invalid": "Nemůžete zrušit neexistující převod repozitáře.", "repo.settings.transfer_abort_success": "Převod repozitáře do %s byl úspěšně zrušen.", "repo.settings.transfer_desc": "Předat tento repozitář uživateli nebo organizaci, ve které máte administrátorská práva.", - "repo.settings.transfer_form_title": "Zadejte jméno repozitáře pro potvrzení:", "repo.settings.transfer_notices_1": "- Ztratíte přístup k repozitáři, pokud jej převedete na uživatele.", "repo.settings.transfer_notices_2": "- Zůstane vám přístup k repozitáři, pokud jej převedete na organizaci kterou (spolu)vlastníte.", "repo.settings.transfer_notices_3": "- Pokud je repozitář soukromý a je předán jednotlivému uživateli, tato akce se ujistí, že uživatel má alespoň oprávnění ke čtení (a v případě potřeby změní oprávnění).", @@ -1983,7 +1950,6 @@ "repo.settings.webhook_deletion": "Odstranit webový háček", "repo.settings.webhook_deletion_desc": "Odstranění webového háčku smaže jeho nastavení a historii doručení. Pokračovat?", "repo.settings.webhook_deletion_success": "Webový háček byl smazán.", - "repo.settings.webhook.test_delivery_desc_disabled": "Chcete-li tento webový háček otestovat s falešnou událostí, aktivujte ho.", "repo.settings.webhook.request": "Požadavek", "repo.settings.webhook.response": "Odpověď", "repo.settings.webhook.headers": "Hlavičky", @@ -1994,8 +1960,6 @@ "repo.settings.webhook.delivery.success": "Událost byla přidána do fronty doručení. Může to trvat několik sekund, než se zobrazí v historii doručení.", "repo.settings.githooks_desc": "Jelikož háčky Gitu jsou spravovány Gitem samotným, můžete upravit soubory háčků k provádění uživatelských operací.", "repo.settings.githook_edit_desc": "Je-li háček neaktivní, bude zobrazen vzorový obsah. Nebude-li zadán žádný obsah, háček bude vypnut.", - "repo.settings.githook_name": "Název háčku", - "repo.settings.githook_content": "Obsah háčku", "repo.settings.update_githook": "Aktualizovat háček", "repo.settings.add_webhook_desc": "Gitea odešle dotaz POST s nastaveným Content Type na cílovou URL. Čtěte více v průvodci webovými háčky.", "repo.settings.payload_url": "Cílové URL", @@ -2160,9 +2124,7 @@ "repo.settings.block_outdated_branch": "Blokovat sloučení, pokud je pull request zastaralý", "repo.settings.block_outdated_branch_desc": "Slučování nebude možné, pokud je hlavní větev za základní větví.", "repo.settings.block_admin_merge_override": "Pravidla pro ochranu větví se vztahují i na administrátory", - "repo.settings.default_branch_desc": "Vybrat výchozí větev repozitáře pro pull requesty a revize kódu:", "repo.settings.merge_style_desc": "Sloučit styly", - "repo.settings.default_merge_style_desc": "Výchozí styl sloučení", "repo.settings.choose_branch": "Vyberte větev…", "repo.settings.no_protected_branch": "Nejsou tu žádné chráněné větve.", "repo.settings.edit_protected_branch": "Upravit", @@ -2264,8 +2226,8 @@ "repo.diff.too_many_files": "Některé soubory nejsou zobrazny, neboť je v této revizi změněno mnoho souborů", "repo.diff.show_more": "Zobrazit více", "repo.diff.load": "Načíst rozdílové porovnání", - "repo.diff.generated": "vygenerováno", - "repo.diff.vendored": "vendorováno", + "repo.diff.generated": "Generováno", + "repo.diff.vendored": "Vendorováno", "repo.diff.comment.add_line_comment": "Přidat jednořádkový komentář", "repo.diff.comment.placeholder": "Zanechat komentář", "repo.diff.comment.add_single_comment": "Přidat jeden komentář", @@ -2281,6 +2243,7 @@ "repo.diff.review.reject": "Požadovat změny", "repo.diff.review.self_approve": "Autoři pull requestu nemohou schválit svůj vlastní pull request", "repo.diff.committed_by": "odevzdal", + "repo.commits.avatar_stack_and": "a", "repo.diff.protected": "Chráněno", "repo.diff.image.side_by_side": "Vedle sebe", "repo.diff.image.swipe": "Posunout", @@ -2358,7 +2321,6 @@ "repo.branch.restore_success": "Větev „%s“ byla obnovena.", "repo.branch.restore_failed": "Nepodařilo se obnovit větev „%s“.", "repo.branch.protected_deletion_failed": "Větev „%s“ je chráněna. Nemůže být smazána.", - "repo.branch.default_deletion_failed": "Větev „%s“ je výchozí větev. Nelze ji odstranit.", "repo.branch.restore": "Obnovit větev „%s“", "repo.branch.download": "Stáhnout větev „%s“", "repo.branch.rename": "Přejmenovat větev „%s“", @@ -2374,7 +2336,6 @@ "repo.branch.new_branch_from": "Vytvořit novou větev z „%s“", "repo.branch.renamed": "Větev %s byla přejmenována na %s.", "repo.branch.rename_default_or_protected_branch_error": "Pouze administrátoři mohou přejmenovat výchozí nebo chráněné větve.", - "repo.branch.rename_protected_branch_failed": "Tato větev je chráněna pravidly ochrany založenými na zástupném vzoru.", "repo.tag.create_tag": "Vytvořit značku %s", "repo.tag.create_tag_operation": "Vytvořit značku", "repo.tag.confirm_create_tag": "Vytvořit značku", @@ -2444,9 +2405,7 @@ "org.settings.labels_desc": "Přidejte štítky, které mohou být použity pro úkoly všech repositářů v rámci této organizace.", "org.members.membership_visibility": "Viditelnost členství:", "org.members.public": "Viditelný", - "org.members.public_helper": "skrýt", "org.members.private": "Skrytý", - "org.members.private_helper": "zviditělnit", "org.members.member_role": "Role člena:", "org.members.owner": "Vlastník", "org.members.member": "Člen", @@ -2477,7 +2436,6 @@ "org.teams.members": "Členové týmu", "org.teams.update_settings": "Upravit nastavení", "org.teams.delete_team": "Smazat tým", - "org.teams.add_team_member": "Přidat člena týmu", "org.teams.invite_team_member": "Pozvat do %s", "org.teams.invite_team_member.list": "Čekající pozvánky", "org.teams.delete_team_title": "Smazat tým", @@ -2503,6 +2461,10 @@ "org.teams.all_repositories_read_permission_desc": "Tomuto týmu je udělen přístup pro Čtení všech repozitářů: členové mohou prohlížet a klonovat repozitáře.", "org.teams.all_repositories_write_permission_desc": "Tomuto týmu je udělen přístup pro Zápis do všech repozitářů: členové mohou prohlížet a nahrávat do repozitářů.", "org.teams.all_repositories_admin_permission_desc": "Tomuto týmu je udělen Administrátorský přístup do všech repozitářů: členové mohou prohlížet, nahrávat a přidávat spolupracovníky do repozitářů.", + "org.teams.visibility": "Viditelnost", + "org.teams.visibility_private": "Soukromý", + "org.teams.visibility_limited": "Omezený", + "org.teams.visibility_public": "Veřejná", "org.teams.invite.title": "Byli jste pozváni do týmu %s v organizaci %s.", "org.teams.invite.by": "Pozvání od %s", "org.teams.invite.description": "Pro připojení k týmu klikněte na tlačítko níže.", @@ -2520,6 +2482,7 @@ "admin.hooks": "Webové háčky", "admin.integrations": "Integrace", "admin.authentication": "Zdroje ověření", + "admin.badges.description": "Popis", "admin.config": "Nastavení", "admin.config_summary": "Souhrn", "admin.config_settings": "Nastavení", @@ -2813,11 +2776,8 @@ "admin.config.server_config": "Nastavení serveru", "admin.config.app_name": "Název stránky", "admin.config.app_ver": "Verze Gitea", - "admin.config.app_url": "Základní URL Gitea", "admin.config.custom_conf": "Cesta ke konfiguračnímu souboru", "admin.config.custom_file_root_path": "Kořenový adresář vlastních souborů", - "admin.config.domain": "Doména serveru", - "admin.config.offline_mode": "Lokální režim", "admin.config.disable_router_log": "Vypnout log směrovače", "admin.config.run_user": "Spustit jako uživatel", "admin.config.run_mode": "Režim spouštění", @@ -2866,7 +2826,6 @@ "admin.config.default_enable_timetracking": "Povolit sledování času ve výchozím nastavení", "admin.config.default_allow_only_contributors_to_track_time": "Povolit sledování času pouze přispěvatelům", "admin.config.no_reply_address": "Skrytá e-mailová doména", - "admin.config.default_visibility_organization": "Výchozí viditelnost pro nové organizace", "admin.config.default_enable_dependencies": "Povolit závislosti úkolů ve výchozím stavu", "admin.config.webhook_config": "Nastavení webových háčků", "admin.config.queue_length": "Délka fronty", @@ -2894,7 +2853,6 @@ "admin.config.cache_config": "Nastavení mezipaměti", "admin.config.cache_adapter": "Adaptér mezipaměti", "admin.config.cache_interval": "Interval mezipaměti", - "admin.config.cache_conn": "Připojení mezipaměti", "admin.config.cache_item_ttl": "Čas vypršení položky v mezipaměti", "admin.config.cache_test": "Otestovat cache", "admin.config.cache_test_failed": "Test cache se nezdařil: %v.", @@ -2902,7 +2860,6 @@ "admin.config.cache_test_succeeded": "Test mezipaměti byl úspěšný, odpověď za %s.", "admin.config.session_config": "Nastavení relace", "admin.config.session_provider": "Poskytovatel relace", - "admin.config.provider_config": "Nastavení poskytovatele", "admin.config.cookie_name": "Název souboru cookie", "admin.config.gc_interval_time": "Čas intervalu GC", "admin.config.session_life_time": "Doba trvání relace", @@ -2910,7 +2867,6 @@ "admin.config.cookie_life_time": "Doba života souboru cookie", "admin.config.picture_config": "Nastavení obrázku a avataru", "admin.config.picture_service": "Služba ikon uživatelů", - "admin.config.disable_gravatar": "Zakázat službu Gravatar", "admin.config.enable_federated_avatar": "Povolit avatary z veřejných zdrojů", "admin.config.open_with_editor_app_help": "Editory \"Otevřít\" v nabídce klon. Ponecháte-li prázdné, bude použito výchozí. Pro zobrazení výchozího nastavení rozbalte.", "admin.config.git_config": "Konfigurace Gitu", @@ -2921,8 +2877,6 @@ "admin.config.git_gc_args": "Parametry GC", "admin.config.git_migrate_timeout": "Časový limit migrace", "admin.config.git_mirror_timeout": "Časový limit aktualizace zrcadla", - "admin.config.git_clone_timeout": "Časový limit operace naklonování", - "admin.config.git_pull_timeout": "Časový limit operace stažení", "admin.config.git_gc_timeout": "Časový limit operace GC", "admin.config.log_config": "Nastavení logů", "admin.config.disabled_logger": "Zakázané", @@ -3159,6 +3113,10 @@ "packages.rubygems.required.rubygems": "Vyžaduje verzi RubyGem", "packages.swift.install": "Přidejte balíček do svého Package.swift souboru:", "packages.swift.install2": "a spustit následující příkaz:", + "packages.terraform.install2": "a spustit následující příkaz:", + "packages.terraform.lock_status": "Stav zámku", + "packages.terraform.lock": "Uzamknout", + "packages.terraform.unlock": "Odemknout", "packages.vagrant.install": "Pro přidání Vagrant box spusťte následující příkaz:", "packages.settings.link": "Propojit tento balíček s repozitářem", "packages.settings.link.select": "Vybrat repozitář", @@ -3262,6 +3220,8 @@ "actions.runs.actor": "Aktér", "actions.runs.actors_no_select": "Všichni aktéři", "actions.runs.status_no_select": "Všechny stavy", + "actions.runs.branch": "Větev", + "actions.runs.branches_no_select": "Všechny větve", "actions.runs.no_results": "Nebyly nalezeny žádné výsledky.", "actions.runs.no_workflows": "Zatím neexistují žádné pracovní postupy.", "actions.runs.no_workflows.quick_start": "Nevíte jak začít s Gitea Actions? Podívejte se na průvodce rychlým startem.", @@ -3269,11 +3229,16 @@ "actions.runs.no_runs": "Pracovní postup zatím nebyl spuštěn.", "actions.runs.empty_commit_message": "(prázdná zpráva commitu)", "actions.runs.expire_log_message": "Logy byly vyčištěny, protože byly příliš staré.", + "actions.runs.summary": "Souhrn", + "actions.runs.latest": "Nejnovější", "actions.workflow.disable": "Zakázat pracovní postup", "actions.workflow.disable_success": "Pracovní postup „%s“ byl úspěšně deaktivován.", "actions.workflow.enable": "Povolit pracovní postup", "actions.workflow.enable_success": "Pracovní postup „%s“ byl úspěšně aktivován.", "actions.workflow.disabled": "Pracovní postup je zakázán.", + "actions.workflow.scope_owner": "Vlastník", + "actions.workflow.scope_global": "Globální", + "actions.workflow.required": "Požadováno", "actions.workflow.run": "Spustit pracovní postup", "actions.workflow.not_found": "Pracovní postup „%s“ nebyl nalezen.", "actions.workflow.run_success": "Pracovní postup „%s“ proběhl úspěšně.", @@ -3303,8 +3268,6 @@ "projects.type-2.display_name": "Projekt repozitíře", "projects.type-3.display_name": "Projekt organizace", "git.filemode.directory": "Adresář", - "git.filemode.normal_file": "Normální soubor", - "git.filemode.executable_file": "Spustitelný soubor", - "git.filemode.symbolic_link": "Symbolický odkaz", - "git.filemode.submodule": "Submodul" + "git.filemode.submodule": "Submodul", + "actions.general.token_permissions.mode.restricted": "Omezený" } diff --git a/options/locale/locale_de-DE.json b/options/locale/locale_de-DE.json index 53d7d4d97d..21c23c9d39 100644 --- a/options/locale/locale_de-DE.json +++ b/options/locale/locale_de-DE.json @@ -3,6 +3,7 @@ "dashboard": "Übersicht", "explore_title": "Erkunden", "help": "Hilfe", + "logo": "Logo", "sign_in": "Anmelden", "sign_in_with_provider": "Anmelden mit %s", "sign_in_or": "oder", @@ -10,7 +11,10 @@ "sign_up": "Registrieren", "link_account": "Account verbinden", "register": "Registrieren", + "version": "Version", + "powered_by": "Unterstützt durch %s", "page": "Seite", + "template": "Vorlage", "language": "Sprache", "notifications": "Benachrichtigungen", "active_stopwatch": "Aktive Zeiterfassung", @@ -28,6 +32,7 @@ "password": "Passwort", "access_token": "Zugangs-Token", "re_type": "Passwort bestätigen", + "captcha": "CAPTCHA", "twofa": "Zwei-Faktor-Authentifizierung", "twofa_scratch": "Zwei-Faktor-Einmalpasswort", "passcode": "PIN", @@ -38,12 +43,15 @@ "webauthn_error": "Dein Sicherheitsschlüssel konnte nicht gelesen werden.", "webauthn_unsupported_browser": "Dein Browser unterstützt derzeit keinen WebAuthn.", "webauthn_error_unknown": "Ein unbekannter Fehler ist aufgetreten. Bitte versuche es erneut.", + "webauthn_error_insecure": "WebAuthn unterstützt nur sichere Verbindungen. Zum Testen über HTTP kannst du \"localhost\" oder \"127.0.0.1\" als Host verwenden.", "webauthn_error_unable_to_process": "Der Server konnte deine Anfrage nicht bearbeiten.", "webauthn_error_duplicated": "Für diese Anfrage ist der Sicherheitsschlüssel nicht erlaubt. Bitte stell sicher, dass er nicht bereits registriert ist.", "webauthn_error_empty": "Du musst einen Namen für diesen Schlüssel festlegen.", "webauthn_error_timeout": "Das Zeitlimit wurde erreicht, bevor dein Schlüssel gelesen werden konnte. Bitte lade die Seite erneut.", "webauthn_reload": "Neu laden", + "repository": "Repository", "organization": "Organisation", + "mirror": "Spiegelserver", "issue_milestone": "Meilenstein", "new_repo": "Neues Repository", "new_migrate": "Neue Migration", @@ -61,22 +69,29 @@ "your_settings": "Einstellungen", "all": "Alle", "sources": "Quellen", + "mirrors": "Spiegelserver", "collaborative": "Kollaborativ", + "forks": "Forks", "activities": "Aktivitäten", "pull_requests": "Pull-Requests", + "issues": "Issues", "milestones": "Meilensteine", + "ok": "OK", "cancel": "Abbrechen", "retry": "Erneut versuchen", "rerun": "Neu starten", "rerun_all": "Alle Jobs neu starten", + "rerun_failed": "Fehlgeschlagene Jobs neu starten", "save": "Speichern", "add": "Hinzufügen", "add_all": "Alle hinzufügen", + "dismiss": "Verwerfen", "remove": "Löschen", "remove_all": "Alle entfernen", "remove_label_str": "Element \"%s \" entfernen", "edit": "Bearbeiten", "view": "Anzeigen", + "test": "Test", "enabled": "Aktiviert", "disabled": "Deaktiviert", "locked": "Gesperrt", @@ -89,29 +104,41 @@ "copy_success": "Kopiert!", "copy_error": "Kopieren fehlgeschlagen", "copy_type_unsupported": "Dieser Dateityp kann nicht kopiert werden", + "copy_filename": "Dateiname kopieren", + "copy_output": "Ausgabe kopieren", "write": "Verfassen", "preview": "Vorschau", "loading": "Laden…", "files": "Dateien", "error_title": "Fehler", "error404": "Die Seite, die Du versuchst aufzurufen, existiert nicht oder Du bist nicht berechtigt, diese anzusehen.", + "error503": "Der Server konnte deine Anfrage nicht abschließen. Bitte versuche es später erneut.", "go_back": "Zurück", "invalid_data": "Ungültige Daten: %v", + "nothing_has_been_changed": "Es wurde nichts geändert.", "never": "Niemals", "unknown": "Unbekannt", + "rss_feed": "RSS Feed", "pin": "Anheften", "unpin": "Loslösen", "artifacts": "Artefakte", "expired": "Abgelaufen", + "artifact_expires_at": "Läuft ab um %s", + "confirm_delete_artifact": "Bist du sicher, dass du das Artefakt '%s' löschen möchtest?", "archived": "Archiviert", + "concept_system_global": "Global", "concept_user_individual": "Individuum", + "concept_code_repository": "Repository", "concept_user_organization": "Organisation", "show_timestamps": "Zeitstempel anzeigen", "show_log_seconds": "Sekunden anzeigen", "show_full_screen": "Vollbild anzeigen", "download_logs": "Logs herunterladen", "confirm_delete_selected": "Alle ausgewählten Elemente löschen?", + "name": "Name", "value": "Wert", + "readme": "Readme", + "filter_title": "Filter", "filter.clear": "Filter leeren", "filter.is_archived": "Archiviert", "filter.not_archived": "Nicht archiviert", @@ -119,26 +146,54 @@ "filter.not_fork": "Kein Fork", "filter.is_mirror": "Gespiegelt", "filter.not_mirror": "Nicht gespiegelt", + "filter.is_template": "Vorlage", "filter.not_template": "Kein Template", "filter.public": "Öffentlich", "filter.private": "Privat", "no_results_found": "Es wurden keine Ergebnisse gefunden.", "internal_error_skipped": "Ein interner Fehler ist aufgetreten, wurde aber übersprungen: %s", + "characters_spaces": "Leerzeichen", + "characters_tabs": "Tabs", + "text_indent_style": "Einrückungsstil", + "text_indent_size": "Einrückungsgröße", + "text_line_wrap": "Zeilenumbruch", + "text_line_nowrap": "Kein Zeilenumbruch", + "text_line_wrap_mode": "Zeilenumbruchmodus", + "search.search": "Suchen…", "search.type_tooltip": "Suchmodus", "search.fuzzy": "Ähnlich", + "search.fuzzy_tooltip": "Ergebnisse einbeziehen, die dem Suchbegriff ähnlich sind", "search.words": "Wörter", "search.words_tooltip": "Nur Suchbegriffe einbeziehen, die den Suchbegriffen exakt entsprechen", + "search.regexp": "Regexp", "search.regexp_tooltip": "Nur Suchbegriffe einbeziehen, die dem Regexp exakt entsprechen", "search.exact": "Exakt", "search.exact_tooltip": "Nur Suchbegriffe einbeziehen, die dem exakten Suchbegriff entsprechen", + "search.repo_kind": "Repositories durchsuchen…", + "search.user_kind": "Benutzer durchsuchen…", + "search.badge_kind": "Badges durchsuchen…", + "search.org_kind": "Organisationen durchsuchen…", + "search.team_kind": "Teams durchsuchen…", + "search.code_kind": "Code durchsuchen…", + "search.code_empty": "Eine Code-Suche starten.", + "search.code_empty_description": "Geben Sie ein Schlüsselwort ein, um den Code zu durchsuchen.", "search.code_search_unavailable": "Zurzeit ist die Code-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator.", "search.code_search_by_git_grep": "Aktuelle Code-Suchergebnisse werden von \"git grep\" bereitgestellt. Es könnte bessere Ergebnisse geben, wenn der Website-Administrator den Repository-Indexer aktiviert.", + "search.package_kind": "Pakete durchsuchen…", + "search.project_kind": "Projekte durchsuchen…", + "search.branch_kind": "Branches durchsuchen…", + "search.tag_kind": "Tags durchsuchen…", "search.tag_tooltip": "Suche nach passenden Tags. Benutze '%', um jede Sequenz von Zahlen zu treffen.", + "search.commit_kind": "Commits durchsuchen…", + "search.runner_kind": "Runner durchsuchen…", "search.no_results": "Es wurden keine passenden Ergebnisse gefunden.", + "search.issue_kind": "Issues durchsuchen…", + "search.pull_kind": "Pull Requests durchsuchen…", "search.keyword_search_unavailable": "Zurzeit ist die Stichwort-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator.", "aria.navbar": "Navigationsleiste", "aria.footer": "Fußzeile", "aria.footer.software": "Über die Software", + "aria.footer.links": "Links", "heatmap.number_of_contributions_in_the_last_12_months": "%s Beiträge in den letzten 12 Monaten", "heatmap.no_contributions": "Keine Beiträge", "heatmap.less": "Weniger", @@ -146,6 +201,7 @@ "editor.buttons.heading.tooltip": "Titel hinzufügen", "editor.buttons.bold.tooltip": "Fettschrift hinzufügen", "editor.buttons.italic.tooltip": "Kursivschrift hinzufügen", + "editor.buttons.strikethrough.tooltip": "Durchgestrichenen Text hinzufügen", "editor.buttons.quote.tooltip": "Text zitieren", "editor.buttons.code.tooltip": "Code hinzufügen", "editor.buttons.link.tooltip": "Link hinzufügen", @@ -161,9 +217,15 @@ "editor.buttons.switch_to_legacy.tooltip": "Legacy-Editor verwenden", "editor.buttons.enable_monospace_font": "Monospace-Schrift aktivieren", "editor.buttons.disable_monospace_font": "Monospace-Schrift deaktivieren", + "editor.code_editor.command_palette": "Befehlspalette", + "editor.code_editor.find": "Suchen", + "editor.code_editor.placeholder": "Dateiinhalt hier eingeben", + "filter.string.asc": "A–Z", + "filter.string.desc": "Z–A", "error.occurred": "Ein Fehler ist aufgetreten", "error.report_message": "Wenn du glaubst, dass dies ein Fehler von Gitea ist, suche bitte auf GitHub nach diesem Fehler und erstelle gegebenenfalls einen neuen Bugreport.", "error.not_found": "Das Ziel konnte nicht gefunden werden.", + "error.permission_denied": "Zugriff verweigert.", "error.network_error": "Netzwerkfehler", "startpage.app_desc": "Ein einfacher, selbst gehosteter Git-Service", "startpage.install": "Einfach zu installieren", @@ -174,23 +236,34 @@ "startpage.lightweight_desc": "Gitea hat minimale Systemanforderungen und kann selbst auf einem günstigen und stromsparenden Raspberry Pi betrieben werden!", "startpage.license": "Quelloffen", "startpage.license_desc": "Hol dir den Code unter %[2]s! Leiste deinen Beitrag bei der Verbesserung dieses Projekts. Trau dich!", + "install.install": "Installation", + "install.installing_desc": "Installation läuft, bitte warten Sie…", "install.title": "Erstkonfiguration", "install.docker_helper": "Wenn du Gitea in einem Docker-Container nutzt, lies bitte die Dokumentation, bevor du irgendwelche Einstellungen veränderst.", "install.require_db_desc": "Gitea benötigt MySQL, PostgreSQL, MSSQL, SQLite3 oder TiDB (MySQL-Protokoll).", "install.db_title": "Datenbankeinstellungen", "install.db_type": "Datenbanktyp", + "install.host": "Host", "install.user": "Benutzername", "install.password": "Passwort", "install.db_name": "Datenbankname", + "install.db_schema": "Schema", "install.db_schema_helper": "Leer lassen, um das Standard-Schema (\"public\") zu verwenden.", + "install.ssl_mode": "SSL", "install.path": "Pfad", "install.sqlite_helper": "Dateipfad zur SQLite3 Datenbank.
Gebe einen absoluten Pfad an, wenn Gitea als Service gestartet wird.", "install.reinstall_error": "Du versuchst, in eine bereits existierende Gitea Datenbank zu installieren", "install.reinstall_confirm_message": "Eine Neuinstallation mit einer bestehenden Gitea-Datenbank kann mehrere Probleme verursachen. In den meisten Fällen solltest du deine vorhandene \"app.ini\" verwenden, um Gitea auszuführen. Wenn du weist, was du tust, bestätigen die folgenden Angaben:", + "install.reinstall_confirm_check_1": "Die von der SECRET_KEY in app.ini verschlüsselten Daten können verloren gehen: Benutzer können sich unter Umständen nicht mit 2FA/OTP einloggen & Mirrors könnten nicht mehr richtig funktionieren. Durch Ankreuzung dieses Kästchens bestätigst du, dass die aktuelle app.ini Datei den korrekten SECRET_KEY enthält.", + "install.reinstall_confirm_check_2": "Die Repositories und Einstellungen müssen eventuell neu synchronisiert werden. Durch das Ankreuzen dieses Kästchens bestätigst du, dass du die Hooks für die Repositories und die authorized_keys Datei manuell neu synchronisierst. Du bestätigst, dass du sicherstellst, dass die Repository- und Spiegel-Einstellungen korrekt sind.", "install.reinstall_confirm_check_3": "Du bestätigst, dass du absolut sicher bist, dass diese Gitea mit der richtigen app.ini läuft, und du sicher bist, dass du neu installieren musst. Du bestätigst, dass du die oben genannten Risiken anerkennst.", "install.err_empty_db_path": "Der SQLite3 Datenbankpfad darf nicht leer sein.", "install.no_admin_and_disable_registration": "Du kannst Selbst-Registrierungen nicht deaktivieren, ohne ein Administratorkonto zu erstellen.", "install.err_empty_admin_password": "Das Administrator-Passwort darf nicht leer sein.", + "install.err_empty_admin_email": "Die E-Mail für das Administratoren-Konto darf nicht leer sein.", + "install.err_admin_name_is_reserved": "Der Benutzername für das Administratoren-Konto ist ungültig. Der Benutzername ist reserviert.", + "install.err_admin_name_pattern_not_allowed": "Der Benutzername für das Administratoren-Konto ist ungültig. Der Benutzername entspricht einem reserviertem Muster.", + "install.err_admin_name_is_invalid": "Der Benutzername für das Administratoren-Konto ist ungültig", "install.general_title": "Allgemeine Einstellungen", "install.app_name": "Seitentitel", "install.app_name_helper": "Du kannst hier den Namen deines Unternehmens eingeben.", @@ -199,12 +272,13 @@ "install.lfs_path": "Git-LFS-Wurzelpfad", "install.lfs_path_helper": "In diesem Verzeichnis werden die Dateien von Git LFS abgespeichert. Leer lassen, um LFS zu deaktivieren.", "install.run_user": "Ausführen als", - "install.run_user_helper": "Der Nutzer unter dem Gitea ausgeführt wird. Beachte, dass dieser Nutzer Zugriff auf das Repository-Wurzelverzeichnis haben muss.", + "install.run_user_helper": "Der Benutzername des Betriebssystems, unter dem Gitea läuft, muss Schreibzugriff auf die Datenpfade haben. Dieser Wert wird automatisch erkannt und kann hier nicht geändert werden. Um einen anderen Benutzer zu verwenden, starten Sie Gitea unter diesem Konto neu.", "install.domain": "Server-Domain", "install.domain_helper": "Domain oder Host-Adresse für den Server.", "install.ssh_port": "SSH-Server-Port", "install.ssh_port_helper": "Der Port deines SSH-Servers. Leer lassen, um SSH zu deaktivieren.", "install.http_port": "Gitea-HTTP-Listen-Port", + "install.http_port_helper": "Port, unter dem der Gitea-Webserver laufen soll.", "install.app_url": "Gitea-Basis-URL", "install.app_url_helper": "Adresse für HTTP(S)-Klon-URLs und E-Mail-Benachrichtigungen.", "install.log_root_path": "Logdateipfad", @@ -221,12 +295,6 @@ "install.register_confirm": "E-Mail-Bestätigung benötigt zum Registrieren", "install.mail_notify": "E-Mail-Benachrichtigungen aktivieren", "install.server_service_title": "Sonstige Server- und Drittserviceeinstellungen", - "install.offline_mode": "Offline-Modus aktivieren", - "install.offline_mode_popup": "Drittanbieter-CDNs deaktivieren und alle Ressourcen lokal zur Verfügung stellen.", - "install.disable_gravatar": "Gravatar deaktivieren", - "install.disable_gravatar_popup": "Gravatar und Drittanbieter-Avatar-Quellen deaktivieren. Ein Standardavatar wird verwendet, bis der Nutzer einen eigenen Avatar hochlädt.", - "install.federated_avatar_lookup": "Föderierte Profilbilder einschalten", - "install.federated_avatar_lookup_popup": "Föderierte Profilbilder via Libravatar aktivieren.", "install.disable_registration": "Registrierung deaktivieren", "install.disable_registration_popup": "Registrierung neuer Benutzer deaktivieren. Nur Administratoren werden neue Benutzerkonten anlegen können.", "install.allow_only_external_registration_popup": "Registrierung nur über externe Services erlauben", @@ -246,12 +314,10 @@ "install.admin_email": "E-Mail-Adresse", "install.install_btn_confirm": "Gitea installieren", "install.test_git_failed": "Fehler beim Test des „git“-Befehls: %v", - "install.sqlite3_not_available": "Diese Gitea-Version unterstützt SQLite3 nicht. Bitte lade die offizielle binäre Version von %s herunter (nicht die „gobuild“-Version).", "install.invalid_db_setting": "Datenbankeinstellungen sind ungültig: %v", "install.invalid_db_table": "Die Datenbanktabelle \"%s\" ist ungültig: %v", "install.invalid_repo_path": "Repository-Verzeichnis ist ungültig: %v", "install.invalid_app_data_path": "Der App-Daten-Pfad ist ungültig: %v", - "install.run_user_not_match": "Der „Ausführen als“-Benutzername ist nicht der aktuelle Benutzername: %s -> %s", "install.internal_token_failed": "Fehler beim Generieren des internen Tokens: %v", "install.secret_key_failed": "Fehler beim Generieren des geheimen Schlüssels: %v", "install.save_config_failed": "Fehler beim Speichern der Konfiguration: %v", @@ -267,6 +333,7 @@ "install.no_reply_address_helper": "Domain-Name für Benutzer mit einer versteckten Emailadresse. Zum Beispiel wird der Benutzername „Joe“ in Git als „joe@noreply.example.org“ protokolliert, wenn die versteckte E-Mail-Domain „noreply.example.org“ festgelegt ist.", "install.password_algorithm": "Passwort Hashing Algorithmus", "install.invalid_password_algorithm": "Ungültiger Passwort-Hash-Algorithmus", + "install.password_algorithm_helper": "Lege einen Passwort-Hashing-Algorithmus fest. Algorithmen haben unterschiedliche Anforderungen und Stärken. Der argon2-Algorithmus ist ziemlich sicher, aber er verbraucht viel Speicher und kann für kleine Systeme ungeeignet sein.", "install.enable_update_checker": "Aktualisierungsprüfung aktivieren", "install.enable_update_checker_helper": "Stellt regelmäßig eine Verbindung zu gitea.io her, um nach neuen Versionen zu prüfen.", "install.env_config_keys": "Umgebungskonfiguration", @@ -276,6 +343,7 @@ "home.uname_holder": "E-Mail-Adresse oder Benutzername", "home.password_holder": "Passwort", "home.switch_dashboard_context": "Kontext der Übersichtsseite wechseln", + "home.my_repos": "Repositories", "home.show_more_repos": "Zeige mehr Repositories…", "home.collaborative_repos": "Gemeinschaftliche Repositories", "home.my_orgs": "Meine Organisationen", @@ -299,9 +367,11 @@ "home.explore_users": "Benutzer erkunden", "home.empty_org": "Es gibt noch keine Organisationen.", "home.empty_repo": "Es gibt noch keine Repositories.", + "explore.repos": "Repositories", "explore.users": "Benutzer", "explore.organizations": "Organisationen", "explore.go_to": "Gehe zu", + "explore.code": "Code", "explore.code_last_indexed_at": "Zuletzt indexiert %s", "explore.relevant_repositories_tooltip": "Repositories, die Forks sind oder die kein Thema, kein Symbol und keine Beschreibung haben, werden ausgeblendet.", "explore.relevant_repositories": "Es werden nur relevante Repositories angezeigt, ungefilterte Ergebnisse anzeigen.", @@ -316,6 +386,7 @@ "auth.forgot_password_title": "Passwort vergessen", "auth.forgot_password": "Passwort vergessen?", "auth.need_account": "Noch kein Konto?", + "auth.sign_up_tip": "Sie registrieren nun das erste Konto im System, welches über Administratorrecht verfügt. Bitte merken Sie sich den Benutzernamen und das Passwort gut. Sollten Sie das ihre Anmeldedaten vergessen, lesen Sie bitte die Dokumentation zu Gitea, um dieses Konto wiederherzustellen.", "auth.sign_up_now": "Jetzt registrieren.", "auth.sign_up_successful": "Konto wurde erfolgreich erstellt. Willkommen!", "auth.confirmation_mail_sent_prompt_ex": "Eine neue Bestätigungs-E-Mail wurde an %sgesendet. Bitte überprüfe deinen Posteingang innerhalb der nächsten %s, um den Registrierungsprozess abzuschließen. Wenn deine Registrierungs-E-Mail-Adresse falsch ist, kannst du dich erneut anmelden und diese ändern.", @@ -324,6 +395,8 @@ "auth.reset_password_mail_sent_prompt": "Eine Bestätigungs-E-Mail wurde an %s gesendet. Bitte überprüfe dein Postfach innerhalb von %s, um den Wiederherstellungsprozess abzuschließen.", "auth.active_your_account": "Aktiviere dein Konto", "auth.account_activated": "Konto wurde aktiviert", + "auth.prohibit_login": "Anmeldung verboten", + "auth.prohibit_login_desc": "Die Anmeldung mit diesem Konto ist nicht gestattet. Bitte kontaktiere den Administrator.", "auth.resent_limit_prompt": "Du hast bereits eine Aktivierungs-E-Mail angefordert. Bitte warte 3 Minuten und probiere es dann nochmal.", "auth.has_unconfirmed_mail": "Hallo %s, du hast eine unbestätigte E-Mail-Adresse (%s). Wenn du keine Bestätigungs-E-Mail erhalten hast oder eine neue senden möchtest, klicke bitte auf den folgenden Button.", "auth.change_unconfirmed_mail_address": "Wenn deine Registrierungs-E-Mail-Adresse falsch ist, kannst du sie hier ändern und eine neue Bestätigungs-E-Mail senden.", @@ -344,7 +417,9 @@ "auth.twofa_scratch_used": "Du hast dein Einmalpasswort verwendet. Du wurdest zu den Einstellung der Zwei-Faktor-Authentifizierung umgeleitet, dort kannst du dein Gerät abmelden oder ein neues Einmalpasswort erzeugen.", "auth.twofa_passcode_incorrect": "Ungültige PIN. Wenn du dein Gerät verloren hast, verwende dein Einmalpasswort.", "auth.twofa_scratch_token_incorrect": "Das Einmalpasswort ist falsch.", + "auth.twofa_required": "Du musst die Zwei-Faktor-Authentifizierung einrichten, um Zugriff auf die Repositories zu erhalten, oder versuche dich erneut anzumelden.", "auth.login_userpass": "Anmelden", + "auth.login_openid": "OpenID", "auth.oauth_signup_tab": "Neues Konto registrieren", "auth.oauth_signup_title": "Neuen Account fertigstellen", "auth.oauth_signup_submit": "Konto vervollständigen", @@ -354,19 +429,24 @@ "auth.oauth.signin.error.general": "Beim Verarbeiten der Autorisierungsanfrage ist ein Fehler aufgetreten: %s. Wenn dieser Fehler weiterhin besteht, wende dich bitte an den Administrator.", "auth.oauth.signin.error.access_denied": "Die Autorisierungsanfrage wurde abgelehnt.", "auth.oauth.signin.error.temporarily_unavailable": "Autorisierung fehlgeschlagen, da der Authentifizierungsserver vorübergehend nicht verfügbar ist. Bitte versuch es später erneut.", + "auth.oauth_callback_unable_auto_reg": "Automatische Registrierung ist aktiviert, aber der OAuth2-Provider %[1]s hat fehlende Felder zurückgegeben: %[2]s, kann den Account nicht automatisch erstellen. Bitte erstelle oder verbinde einen Account oder kontaktiere den Administrator.", "auth.openid_connect_submit": "Verbinden", "auth.openid_connect_title": "Mit bestehendem Konto verbinden", "auth.openid_connect_desc": "Die gewählte OpenID-URI ist unbekannt. Ordne sie hier einem neuen Account zu.", "auth.openid_register_title": "Neues Konto einrichten", "auth.openid_register_desc": "Die gewählte OpenID-URI ist unbekannt. Ordne sie hier einem neuen Account zu.", "auth.openid_signin_desc": "Gib deine OpenID-URI ein, zum Beispiel alice.openid.example.org oder https://openid.example.org/alice.", + "auth.disable_forgot_password_mail": "Die Kontowiederherstellung ist nicht möglich, da keine E-Mail-Adresse für dieses Konto eingerichtet ist. Bitte kontaktieren Sie ihren Administrator.", + "auth.disable_forgot_password_mail_admin": "Die Kontowiederherstellung ist nur verfügbar, wenn eine E-Mail-Adresse eingerichtet ist.", "auth.email_domain_blacklisted": "Du kannst dich nicht mit deiner E-Mail-Adresse registrieren.", "auth.authorize_application": "Anwendung autorisieren", "auth.authorize_redirect_notice": "Du wirst zu %s weitergeleitet, wenn du diese Anwendung autorisierst.", "auth.authorize_application_created_by": "Diese Anwendung wurde von %s erstellt.", + "auth.authorize_application_description": "Wenn du den Zugriff gewährst, wird diese Anwendung Lese- und Schreibzugriff auf alle deine Kontoinformationen erhalten, sowie auch auf private Repositories und Organisationen.", "auth.authorize_application_with_scopes": "Mit Bereichen: %s", "auth.authorize_title": "\"%s\" den Zugriff auf deinen Account gestatten?", "auth.authorization_failed": "Autorisierung fehlgeschlagen", + "auth.authorization_failed_desc": "Die Autorisierung ist fehlgeschlagen, da eine ungültige Anfrage erkannt wurde. Bitte kontaktiere den Betreuer der Anwendung, die du gerade Autorisieren wolltest.", "auth.sspi_auth_failed": "SSPI-Authentifizierung fehlgeschlagen", "auth.password_pwned": "Das von dir gewählte Passwort befindet sich auf einer Liste gestohlener Passwörter, die öffentlich verfügbar sind. Bitte versuche es erneut mit einem anderen Passwort und ziehe in Erwägung, auch anderswo deine Passwörter zu ändern.", "auth.password_pwned_err": "Anfrage an HaveIBeenPwned konnte nicht abgeschlossen werden", @@ -386,6 +466,8 @@ "mail.activate_email.text": "Bitte klicke innerhalb von %s auf folgenden Link, um dein Konto zu aktivieren:", "mail.register_notify": "Willkommen bei %s", "mail.register_notify.title": "%[1]s, willkommen bei %[2]s", + "mail.register_notify.text_1": "Dies ist deine Bestätigungs-E-Mail zur Registrierung bei %s!", + "mail.register_notify.text_2": "Du kannst dich nun mit diesem Benutzernamen anmelden: %s.", "mail.register_notify.text_3": "Wenn dieser Account von dir erstellt wurde, musst du zuerst dein Passwort setzen.", "mail.reset_password": "Stelle dein Konto wieder her", "mail.reset_password.title": "%s, du hast um Wiederherstellung deines Kontos gebeten", @@ -406,17 +488,27 @@ "mail.issue.action.review_dismissed": "@%[1]s hat das letzte Review von %[2]s für diesen Pull Request verworfen.", "mail.issue.action.ready_for_review": "@%[1]s hat diesen Pull Request zum Review freigegeben.", "mail.issue.action.new": "@%[1]s hat #%[2]d geöffnet.", + "mail.issue.in_tree_path": "In %s:", "mail.release.new.subject": "Release %s in %s erschienen", "mail.release.new.text": "@%[1]s hat %[2]s in %[3]s released", "mail.release.title": "Titel: %s", "mail.release.note": "Anmerkung:", + "mail.release.downloads": "Downloads:", "mail.release.download.zip": "Quellcode (ZIP Datei)", "mail.release.download.targz": "Quellcode (TAR.GZ Datei)", "mail.repo.transfer.subject_to": "%s möchte \"%s\" an %s übertragen", "mail.repo.transfer.subject_to_you": "%s möchte dir \"%s\" übertragen", "mail.repo.transfer.to_you": "dir", + "mail.repo.transfer.body": "Um die Anfrage zu akzeptieren oder abzulehnen, besuche %s oder ignoriere diese Nachricht.", "mail.repo.collaborator.added.subject": "%s hat dich zu %s hinzugefügt", "mail.repo.collaborator.added.text": "Du wurdest als Mitarbeiter für folgendes Repository hinzugefügt:", + "mail.repo.actions.run.failed": "Ausführung fehlgeschlagen", + "mail.repo.actions.run.succeeded": "Ausführung erfolgreich", + "mail.repo.actions.run.cancelled": "Ausführung abgebrochen", + "mail.repo.actions.jobs.all_succeeded": "Alle Jobs wurden erfolgreich ausgeführt", + "mail.repo.actions.jobs.all_failed": "Alle Jobs sind fehlgeschlagen", + "mail.repo.actions.jobs.some_not_successful": "Manche Jobs waren nicht erfolgreich", + "mail.repo.actions.jobs.all_cancelled": "Alle Jobs wurden abgebrochen", "mail.team_invite.subject": "%[1]s hat dich eingeladen, der Organisation %[2]s beizutreten", "mail.team_invite.text_1": "%[1]s hat dich eingeladen, dem Team %[2]s in der Organisation %[3]s beizutreten.", "mail.team_invite.text_2": "Bitte klicke auf den folgenden Link, um dem Team beizutreten:", @@ -457,6 +549,8 @@ "form.include_error": " muss den Text \"%s\" enthalten.", "form.glob_pattern_error": " Der Glob Pattern ist ungültig: %s.", "form.regex_pattern_error": " regex ist ungültig: %s.", + "form.username_error": " darf nur alphanumerische Zeichen ('0-9','a-z','A-Z'), Bindestriche ('-'), Unterstriche ('_') und Punkte ('.') enthalten. Es darf nicht mit nicht-alphanumerischen Zeichen beginnen oder enden und aufeinanderfolgende nicht-alphanumerische Zeichen sind ebenfalls verboten.", + "form.invalid_slug_error": " ist ungültig.", "form.invalid_group_team_map_error": " Zuordnung ist ungültig: %s", "form.unknown_error": "Unbekannter Fehler:", "form.captcha_incorrect": "Der eingegebene CAPTCHA-Code ist falsch.", @@ -470,14 +564,17 @@ "form.repo_name_been_taken": "Der Repository-Name wird schon verwendet.", "form.repository_force_private": "Privat erzwingen ist aktiviert: Private Repositories können nicht veröffentlicht werden.", "form.repository_files_already_exist": "Dateien für dieses Repository sind bereits vorhanden. Kontaktiere den Systemadministrator.", + "form.repository_files_already_exist.adopt": "Dateien für dieses Repository existieren bereits und können nur übernommen werden.", "form.repository_files_already_exist.delete": "Dateien für dieses Repository sind bereits vorhanden. Du must sie löschen.", "form.repository_files_already_exist.adopt_or_delete": "Dateien für dieses Repository existieren bereits. Du musst sie entweder übernehmen oder löschen.", "form.visit_rate_limit": "Das Rate-Limit bei der Gegenseite wurde erreicht.", + "form.2fa_auth_required": "Die Gegenseite benötigt Zweifaktorauthentifikation.", "form.org_name_been_taken": "Der Organisationsname ist bereits vergeben.", "form.team_name_been_taken": "Der Teamname ist bereits vergeben.", "form.team_no_units_error": "Das Team muss auf mindestens einen Bereich Zugriff haben.", "form.email_been_used": "Die E-Mail-Adresse wird bereits verwendet.", "form.email_invalid": "Die E-Mail-Adresse ist ungültig.", + "form.email_domain_is_not_allowed": "Die Domäne der Benutzer-E-Mail %s steht im Widerspruch zur EMAIL_DOMAIN_ALLOWLIST oder EMAIL_DOMAIN_BLOCKLIST. Bitte stelle sicher, dass diese Operation gewollt ist.", "form.openid_been_used": "Die OpenID-Adresse \"%s\" wird bereits verwendet.", "form.username_password_incorrect": "Benutzername oder Passwort ist falsch.", "form.password_complexity": "Das Passwort erfüllt nicht die Komplexitätsanforderungen:", @@ -501,12 +598,19 @@ "form.invalid_gpg_key": "Dein GPG-Key kann nicht überprüft werden: %s", "form.invalid_ssh_principal": "Ungültige Identität: %s", "form.must_use_public_key": "Der von Dir bereitgestellte Key ist ein privater Key. Bitte lade Deinen privaten Key nirgendwo hoch. Verwende stattdessen Deinen öffentlichen Key.", + "form.unable_verify_ssh_key": "Der SSH-Key kann nicht verifiziert werden. Überprüfe ihn auf Fehler.", "form.auth_failed": "Authentifizierung fehlgeschlagen: %v", + "form.still_own_repo": "Dein Konto besitzt ein oder mehrere Repositories. Diese müssen gelöscht oder an andere übertragen werden.", + "form.still_has_org": "Dein Konto ist ein Mitglied von einer oder mehreren Organisationen. Verlasse diese, bevor du fortfährst.", + "form.still_own_packages": "Dein Konto besitzt ein oder mehrere Pakete, lösche diese zuerst.", + "form.org_still_own_repo": "Diese Organisation besitzt noch ein oder mehrere Repositories. Diese müssen zuerst gelöscht oder übertragen werden.", + "form.org_still_own_packages": "Diese Organisation besitzt noch ein oder mehrere Pakete, lösche diese zuerst.", "form.target_branch_not_exist": "Der Ziel-Branch existiert nicht.", "form.target_ref_not_exist": "Zielreferenz existiert nicht %s", "form.admin_cannot_delete_self": "Du kannst dich nicht selbst löschen, wenn du ein Administrator bist. Bitte entferne zuerst deine Administratorrechte.", "user.change_avatar": "Profilbild ändern…", "user.joined_on": "Beigetreten am %s", + "user.repositories": "Repositories", "user.activity": "Öffentliche Aktivität", "user.followers": "Follower", "user.show_more": "Mehr anzeigen", @@ -526,20 +630,17 @@ "user.settings": "Benutzereinstellungen", "user.form.name_reserved": "Der Benutzername \"%s\" ist reserviert.", "user.form.name_pattern_not_allowed": "Das Muster \"%s\" ist nicht in einem Benutzernamen erlaubt.", + "user.form.name_chars_not_allowed": "Benutzername \"%s\" enthält ungültige Zeichen.", "user.block.block": "Sperren", "user.block.block.user": "Benutzer sperren", + "user.block.block.org": "Benutzer von Organisation sperren", "user.block.block.failure": "Fehler beim Sperren des Benutzers: %s", "user.block.unblock": "Entsperren", "user.block.unblock.failure": "Fehler beim Entsperren des Benutzers: %s", "user.block.blocked": "Du hast diesen Benutzer gesperrt.", "user.block.title": "Einen Benutzer sperren", "user.block.info": "Das Blockieren eines Benutzers hindert ihn daran, mit Repositories zu interagieren, wie zum Beispiel das Öffnen oder Kommentieren von Pull Requests oder Issues. Erfahre mehr über das Blockieren eines Benutzers.", - "user.block.info_1": "Das Blockieren eines Benutzers verhindert folgende Aktionen auf deinem Konto und deinen Repositories:", - "user.block.info_2": "deinem Konto folgen", - "user.block.info_3": "dir Benachrichtigungen durch @Erwähnung deines Benutzernamens senden", - "user.block.info_4": "dich als Mitarbeiter in deren Repositories einladen", - "user.block.info_5": "Repositories favorisieren, forken oder beobachten", - "user.block.info_6": "Issues oder Pull Requests öffnen und kommentieren", + "user.block.info.docs": "Erfahren Sie mehr über das Blockieren eines Benutzers.", "user.block.user_to_block": "Zu sperrender Benutzer", "user.block.note": "Anmerkung", "user.block.note.title": "Optionale Anmerkung:", @@ -547,7 +648,9 @@ "user.block.note.edit": "Anmerkung bearbeiten", "user.block.list": "Gesperrte Benutzer", "user.block.list.none": "Du hast noch keine Benutzer gesperrt.", + "settings.general": "Allgemein", "settings.profile": "Profil", + "settings.account": "Konto", "settings.appearance": "Erscheinung", "settings.password": "Passwort", "settings.security": "Sicherheit", @@ -556,14 +659,17 @@ "settings.social": "Soziale Konten", "settings.applications": "Anwendungen", "settings.orgs": "Organisationen verwalten", + "settings.repos": "Repositories", "settings.delete": "Konto löschen", "settings.twofa": "Zwei-Faktor-Authentifizierung (TOTP)", "settings.account_link": "Verknüpfte Benutzerkonten", "settings.organization": "Organisationen", + "settings.uid": "UID", "settings.webauthn": "Zwei-Faktor-Authentifizierung (Hardware-Sicherheitsschlüssel)", "settings.public_profile": "Öffentliches Profil", "settings.biography_placeholder": "Erzähle uns ein wenig über Dich selbst! (Du kannst Markdown verwenden)", "settings.location_placeholder": "Teile Deinen ungefähren Standort mit anderen", + "settings.profile_desc": "Lege fest, wie dein Profil anderen Benutzern angezeigt wird. Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und webbasierte Git-Operationen verwendet.", "settings.password_username_disabled": "Du bist nicht berechtigt, den Benutzernamen zu ändern. Bitte kontaktiere Deinen Seitenadministrator für weitere Details.", "settings.password_full_name_disabled": "Du bist nicht berechtigt, den vollständigen Namen zu ändern. Bitte kontaktiere Deinen Seitenadministrator für weitere Details.", "settings.full_name": "Vollständiger Name", @@ -581,13 +687,17 @@ "settings.continue": "Weiter", "settings.cancel": "Abbrechen", "settings.language": "Sprache", + "settings.ui": "Design", "settings.hidden_comment_types": "Ausgeblendeter Kommentartypen", + "settings.hidden_comment_types_description": "Die hier markierten Kommentartypen werden nicht innerhalb der Issue-Seiten angezeigt. Beispielsweise entfernt das Markieren von \"Label\" alle \"{user} hat {label} hinzugefügt/entfernt\"-Kommentare.", "settings.hidden_comment_types.ref_tooltip": "Kommentare, in denen dieses Issue von einem anderen Issue/Commit referenziert wurde", "settings.hidden_comment_types.issue_ref_tooltip": "Kommentare, bei denen der Benutzer den Branch/Tag des Issues ändert", "settings.comment_type_group_reference": "Verweis auf Mitglieder", + "settings.comment_type_group_label": "Label", "settings.comment_type_group_milestone": "Meilenstein", "settings.comment_type_group_assignee": "Zuweisung", "settings.comment_type_group_title": "Titel", + "settings.comment_type_group_branch": "Branch", "settings.comment_type_group_time_tracking": "Zeiterfassung", "settings.comment_type_group_deadline": "Frist", "settings.comment_type_group_dependency": "Abhängigkeit", @@ -624,14 +734,18 @@ "settings.manage_openid": "OpenID-Adressen verwalten", "settings.email_desc": "Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und, sofern sie nicht versteckt ist, web-basierte Git-Operationen verwendet.", "settings.theme_desc": "Dies wird dein Standard-Theme auf der Seite sein.", + "settings.theme_colorblindness_help": "Hilfe zum Design für Farbenblinde", + "settings.theme_colorblindness_prompt": "Gitea hat nur ein paar Designs mit einfacher Farbblindheit, die nur wenige Farben definiert haben. Die Arbeit ist noch im Gange. Weitere Verbesserungen können durch die Definition von mehr Farben in den CSS-Dateien vorgenommen werden.", "settings.primary": "Primär", "settings.activated": "Aktiviert", "settings.requires_activation": "Erfordert Aktivierung", "settings.primary_email": "Als primäre E-Mail-Adresse verwenden", "settings.activate_email": "Aktivierung senden", "settings.activations_pending": "Aktivierung ausstehend", + "settings.can_not_add_email_activations_pending": "Es gibt eine ausstehende Aktivierung, versuche es in ein paar Minuten erneut, wenn du eine neue E-Mail hinzufügen möchtest.", "settings.delete_email": "Löschen", "settings.email_deletion": "E-Mail-Adresse löschen", + "settings.email_deletion_desc": "Diese E-Mail-Adresse und damit verbundene Informationen werden von Ihrem Konto entfernt. Git Commits durch diese E-Mail-Adresse bleiben unverändert. Fortfahren?", "settings.email_deletion_success": "Die E-Mail-Adresse wurde entfernt.", "settings.theme_update_success": "Deine Theme-Auswahl wurde gespeichert.", "settings.theme_update_error": "Das ausgewählte Theme existiert nicht.", @@ -643,6 +757,7 @@ "settings.add_email": "E-Mail-Adresse hinzufügen", "settings.add_openid": "OpenID-URI hinzufügen", "settings.add_email_confirmation_sent": "Eine Bestätigungs-E-Mail wurde an \"%s\" gesendet. Bitte überprüfe dein Postfach innerhalb der nächsten %s, um die E-Mail-Adresse zu bestätigen.", + "settings.email_primary_not_found": "Die ausgewählte E-Mail Adresse konnte nicht gefunden werden.", "settings.add_email_success": "Die neue E-Mail-Addresse wurde hinzugefügt.", "settings.email_preference_set_success": "E-Mail-Einstellungen wurden erfolgreich aktualisiert.", "settings.add_openid_success": "Die neue OpenID-Adresse wurde hinzugefügt.", @@ -673,7 +788,9 @@ "settings.gpg_key_verified": "Verifizierter Schlüssel", "settings.gpg_key_verified_long": "Der Schlüssel wurde mit einem Token verifiziert. Er kann verwendet werden, um Commits zu verifizieren, die mit irgendeiner für diesen Nutzer aktivierten E-Mail-Adresse und irgendeiner Identität dieses Schlüssels übereinstimmen.", "settings.gpg_key_verify": "Verifizieren", + "settings.gpg_invalid_token_signature": "Der GPG Key, die Signatur, und das Token stimmen nicht überein, oder das Token ist veraltet.", "settings.gpg_token_required": "Du musst eine Signatur für das folgende Token angeben", + "settings.gpg_token": "Token", "settings.gpg_token_help": "Du kannst eine Signatur wie folgt generieren:", "settings.gpg_token_signature": "GPG Textsignatur (armored signature)", "settings.key_signature_gpg_placeholder": "Beginnt mit '-----BEGIN PGP SIGNATURE-----'", @@ -681,7 +798,9 @@ "settings.ssh_key_verified": "Verifizierter Schlüssel", "settings.ssh_key_verified_long": "Der Schlüssel wurde mit einem Token verifiziert. Er kann verwendet werden, um Commits zu verifizieren, die mit irgendeiner für diesen Nutzer aktivierten E-Mail-Adresse und irgendeiner Identität dieses Schlüssels übereinstimmen.", "settings.ssh_key_verify": "Verifizieren", + "settings.ssh_invalid_token_signature": "Der SSH Key, die Signatur, und das Token stimmen nicht überein, oder das Token ist veraltet.", "settings.ssh_token_required": "Du musst eine Signatur für den Token unten angeben", + "settings.ssh_token": "Token", "settings.ssh_token_help": "Du kannst eine Signatur wie folgt generieren:", "settings.ssh_token_signature": "SSH Textsignatur (armored signature)", "settings.key_signature_ssh_placeholder": "Beginnt mit „-----BEGIN SSH SIGNATURE-----“", @@ -700,6 +819,7 @@ "settings.ssh_principal_deletion": "SSH-Zertifik-Identität entfernen", "settings.ssh_key_deletion_desc": "Wenn du einen SSH-Key entfernst, hast du mit diesem Key keinen Zugriff mehr. Fortfahren?", "settings.gpg_key_deletion_desc": "Wenn du einen GPG-Schlüssel entfernst, können damit unterschriebene Commits nicht mehr verifiziert werden. Fortfahren?", + "settings.ssh_principal_deletion_desc": "Das Entfernen einer SSH-Zertifikat-Identität entzieht den Zugriff auf dein Konto. Fortfahren?", "settings.ssh_key_deletion_success": "Der SSH-Schlüssel wurde entfernt.", "settings.gpg_key_deletion_success": "Der GPG-Schlüssel wurde entfernt.", "settings.ssh_principal_deletion_success": "Die Identität wurde entfernt.", @@ -759,6 +879,7 @@ "settings.create_oauth2_application_success": "Du hast erfolgreich eine neue OAuth2-Anwendung erstellt.", "settings.update_oauth2_application_success": "Du hast die OAuth2-Anwendung erfolgreich aktualisiert.", "settings.oauth2_application_name": "Name der Anwendung", + "settings.oauth2_confidential_client": "Vertraulicher Client. Für Anwendungen aktivieren, die das Geheimnis sicher speichern, z. B. Webanwendungen. Wähle diese Option nicht für native Anwendungen für Desktop und Mobilgeräte.", "settings.oauth2_skip_secondary_authorization": "Autorisierung für öffentliche Clients nach einmaliger Gewährung des Zugriffs überspringen. Dies kann ein Sicherheitsrisiko darstellen.", "settings.oauth2_redirect_uris": "URIs für die Weiterleitung. Bitte verwende eine neue Zeile für jede URI.", "settings.save_application": "Speichern", @@ -772,8 +893,10 @@ "settings.oauth2_application_remove_description": "Das Entfernen einer OAuth2-Anwendung hat zur Folge, dass diese nicht mehr auf autorisierte Benutzeraccounts auf dieser Instanz zugreifen kann. Möchtest Du fortfahren?", "settings.oauth2_application_locked": "Wenn es in der Konfiguration aktiviert ist, registriert Gitea einige OAuth2-Anwendungen beim Starten vor. Um unerwartetes Verhalten zu verhindern, können diese weder bearbeitet noch entfernt werden. Weitere Informationen findest Du in der OAuth2-Dokumentation.", "settings.authorized_oauth2_applications": "Autorisierte OAuth2-Anwendungen", + "settings.authorized_oauth2_applications_description": "Den folgenden Drittanbieter-Apps hast Du Zugriff auf Deinen persönlichen Gitea-Account gewährt. Bitte widerrufe die Autorisierung für Apps, die Du nicht mehr nutzt.", "settings.revoke_key": "Widerrufen", "settings.revoke_oauth2_grant": "Autorisierung widerrufen", + "settings.revoke_oauth2_grant_description": "Wenn du die Autorisierung widerrufst, kann die Anwendung nicht mehr auf deine Daten zugreifen. Bist du dir sicher?", "settings.revoke_oauth2_grant_success": "Zugriff erfolgreich widerrufen.", "settings.twofa_desc": "Um dein Konto vor Passwortdiebstahl zu schützen, kannst du ein Smartphone oder ein anderes Gerät verwenden, um zeitbasierte Einmalpasswörter (\"TOTP\") zu erhalten.", "settings.twofa_recovery_tip": "Wenn du dein Gerät verlierst, kannst du einen einmalig verwendbaren Wiederherstellungsschlüssel nutzen, um den Zugriff auf dein Konto wiederherzustellen.", @@ -782,6 +905,7 @@ "settings.twofa_disable": "Zwei-Faktor-Authentifizierung deaktivieren", "settings.twofa_scratch_token_regenerate": "Einweg-Wiederherstellungsschlüssel neu generieren", "settings.twofa_scratch_token_regenerated": "Dein Einweg-Wiederherstellungsschlüssel ist jetzt %s. Speichere ihn an einem sicheren Ort, er wird nie wieder angezeigt.", + "settings.twofa_enroll": "Zwei-Faktor-Authentifizierung aktivieren", "settings.twofa_disable_note": "Du kannst die Zwei-Faktor-Authentifizierung auch wieder deaktivieren.", "settings.twofa_disable_desc": "Wenn du die Zwei-Faktor-Authentifizierung deaktivierst, wird die Sicherheit deines Kontos verringert. Fortfahren?", "settings.regenerate_scratch_token_desc": "Wenn du deinen Wiederherstellungsschlüssel verlegt oder bereits benutzt hast, kannst du ihn hier zurücksetzen.", @@ -794,11 +918,14 @@ "settings.twofa_failed_get_secret": "Fehler beim Abrufen des Secrets.", "settings.webauthn_desc": "Sicherheitsschlüssel sind Geräte, die kryptografische Schlüssel beeinhalten. Diese können für die Zwei-Faktor-Authentifizierung verwendet werden. Der Sicherheitsschlüssel muss den Standard \"WebAuthn\" unterstützen.", "settings.webauthn_register_key": "Sicherheitsschlüssel hinzufügen", + "settings.webauthn_nickname": "Benutzername", "settings.webauthn_delete_key": "Sicherheitsschlüssel entfernen", + "settings.webauthn_delete_key_desc": "Wenn du einen Sicherheitsschlüssel entfernst, kannst du dich nicht mehr mit ihm anmelden. Fortfahren?", "settings.webauthn_key_loss_warning": "Wenn du deine Sicherheitsschlüssel verlierst, verlierst du den Zugriff auf dein Konto.", "settings.webauthn_alternative_tip": "Möglicherweise möchtest du eine zusätzliche Authentifizierungsmethode konfigurieren.", "settings.manage_account_links": "Verknüpfte Accounts verwalten", "settings.manage_account_links_desc": "Diese externen Accounts sind mit deinem Gitea-Account verknüpft.", + "settings.account_links_not_available": "Derzeit sind keine externen Konten mit deinem Gitea-Konto verknüpft.", "settings.link_account": "Account verbinden", "settings.remove_account_link": "Verknüpften Account entfernen", "settings.remove_account_link_desc": "Wenn du den verknüpften Account entfernst, wirst du darüber nicht mehr auf deinen Gitea-Account zugreifen können. Fortfahren?", @@ -817,6 +944,8 @@ "settings.email_notifications.disable": "E-Mail Benachrichtigungen deaktivieren", "settings.email_notifications.submit": "E-Mail-Einstellungen festlegen", "settings.email_notifications.andyourown": "Und deine Eigenen Benachrichtigungen", + "settings.email_notifications.actions.desc": "Benachrichtigungen für Workflow laufen auf Repositorys, die mit Gitea Actions eingerichtet wurden.", + "settings.email_notifications.actions.failure_only": "Nur über fehlgeschlagene Workflows benachrichtigen", "settings.visibility": "Nutzer Sichtbarkeit", "settings.visibility.public": "Öffentlich", "settings.visibility.public_tooltip": "Für alle sichtbar", @@ -828,8 +957,11 @@ "repo.owner": "Besitzer", "repo.owner_helper": "Einige Organisationen könnten in der Dropdown-Liste nicht angezeigt werden, da die Anzahl an Repositories begrenzt ist.", "repo.repo_name": "Repository-Name", + "repo.repo_name_profile_public_hint": ".profile ist ein spezielles Repository, mit dem du die README.md zu deinem öffentlichen Organisationsprofil hinzufügen kannst, das für jeden sichtbar ist. Stelle sicher, dass es öffentlich ist und initialisiere es mit einer README im Profilverzeichnis, um loszulegen.", + "repo.repo_name_profile_private_hint": ".profile-private ist ein spezielles Repository, mit dem du die README.md zu deinem privaten Organisationsprofil hinzufügen kannst, das nur für Organisationsmitglieder sichtbar ist. Stelle sicher, dass es privat ist und initialisiere es mit einer README im Profilverzeichnis, um loszulegen.", "repo.repo_name_helper": "Ein guter Repository-Name besteht normalerweise aus kurzen, unvergesslichen und einzigartigen Schlagwörtern. Ein Repository namens \".profile\" or \".profile-private\" kann verwendet werden, um die README.md auf dem Benutzer- oder Organisationsprofil anzuzeigen.", "repo.repo_size": "Repository-Größe", + "repo.template": "Vorlage", "repo.template_select": "Vorlage auswählen", "repo.template_helper": "Repository zu einem Template machen", "repo.template_description": "Template-Repositories erlauben es Benutzern, neue Repositories mit den gleichen Verzeichnisstrukturen, Dateien und optionalen Einstellungen zu erstellen.", @@ -837,7 +969,6 @@ "repo.visibility_description": "Nur der Besitzer oder Organisationsmitglieder mit entsprechender Berechtigung, werden in der Lage sein, es zu sehen.", "repo.visibility_helper": "In privates Repository umwandeln", "repo.visibility_helper_forced": "Auf dieser Gitea-Instanz können nur private Repositories angelegt werden.", - "repo.visibility_fork_helper": "(Eine Änderung dieses Wertes wirkt sich auf alle Forks aus)", "repo.clone_helper": "Benötigst du Hilfe beim Klonen? Öffne die Hilfe.", "repo.fork_repo": "Repository forken", "repo.fork_from": "Fork von", @@ -848,9 +979,11 @@ "repo.all_branches": "Alle Branches", "repo.view_all_branches": "Alle Branches anzeigen", "repo.view_all_tags": "Alle Tags anzeigen", + "repo.fork_no_valid_owners": "Dieses Repository kann nicht geforkt werden, da keine gültigen Besitzer vorhanden sind.", "repo.fork.blocked_user": "Das Repository kann nicht geforkt werden, da du vom Repository-Eigentümer blockiert wurdest.", "repo.use_template": "Dieses Template verwenden", "repo.open_with_editor": "Mit %s öffnen", + "repo.download_directory_as": "Verzeichnis als %s herunterladen", "repo.download_zip": "ZIP herunterladen", "repo.download_tar": "TAR.GZ herunterladen", "repo.download_bundle": "BUNDLE herunterladen", @@ -870,6 +1003,7 @@ "repo.multiple_licenses": "Mehrere Lizenzen", "repo.object_format": "Objektformat", "repo.object_format_helper": "Objektformat des Repositories. Es kann später nicht geändert werden. SHA1 ist am meisten kompatibel.", + "repo.readme": "README", "repo.readme_helper": "Wähle eine README-Vorlage aus.", "repo.readme_helper_desc": "Hier kannst du eine komplette Beschreibung für dein Projekt schreiben.", "repo.auto_init": "Repository initialisieren (Fügt .gitignore, License und README-Dateien hinzu)", @@ -890,10 +1024,12 @@ "repo.mirror_sync_on_commit": "Synchronisieren, wenn Commits gepusht wurden", "repo.mirror_address": "Klonen via URL", "repo.mirror_address_desc": "Gib alle erforderlichen Anmeldedaten im Abschnitt \"Authentifizierung\" ein.", + "repo.mirror_address_url_invalid": "Die angegebene URL ist ungültig. Achte darauf, alle URL-Komponenten korrekt zu maskieren.", "repo.mirror_address_protocol_invalid": "Die angegebene URL ist ungültig. Nur URLs beginnend mit http(s):// oder git:// sind möglich.", "repo.mirror_lfs": "Großdatei-Speicher (LFS)", "repo.mirror_lfs_desc": "Mirroring von LFS-Dateien aktivieren.", "repo.mirror_lfs_endpoint": "LFS-Endpunkt", + "repo.mirror_lfs_endpoint_desc": "Sync wird versuchen, die Klon-URL zu verwenden, um den LFS-Server zu bestimmen. Du kannst auch einen eigenen Endpunkt angeben, wenn die LFS-Dateien woanders gespeichert werden.", "repo.mirror_last_synced": "Zuletzt synchronisiert", "repo.mirror_password_placeholder": "(unverändert)", "repo.mirror_password_blank_placeholder": "(Nicht gesetzt)", @@ -901,10 +1037,13 @@ "repo.watchers": "Beobachter", "repo.stargazers": "Favorisiert von", "repo.stars_remove_warning": "Dies wird alle Sterne aus diesem Repository entfernen.", + "repo.forks": "Forks", "repo.stars": "Favoriten", "repo.reactions_more": "und %d weitere", + "repo.reactions": "Reaktionen", "repo.unit_disabled": "Der Administrator hat diesen Repository-Bereich deaktiviert.", "repo.language_other": "Andere", + "repo.adopt_search": "Geben einen Benutzernamen ein, um nach nicht angenommenen Repositories zu suchen... (leer lassen um alle zu finden)", "repo.adopt_preexisting_label": "Dateien übernehmen", "repo.adopt_preexisting": "Vorhandene Dateien übernehmen", "repo.adopt_preexisting_content": "Repository aus %s erstellen", @@ -922,22 +1061,27 @@ "repo.transfer.accept_desc": "Übertragung nach \"%s\"", "repo.transfer.reject": "Übertragung Ablehnen", "repo.transfer.reject_desc": "Übertragung nach \"%s \" abbrechen", - "repo.transfer.no_permission_to_accept": "Du hast keine Berechtigung, diesen Transfer anzunehmen.", - "repo.transfer.no_permission_to_reject": "Du hast keine Berechtigung, diesen Transfer abzulehnen.", + "repo.transfer.is_transferring": "Übertragung…", + "repo.transfer.is_transferring_prompt": "Das Repository wurde nach %s transferiert", "repo.desc.private": "Privat", "repo.desc.public": "Öffentlich", "repo.desc.public_access": "Öffentlicher Zugriff", + "repo.desc.template": "Vorlage", "repo.desc.internal": "Intern", "repo.desc.archived": "Archiviert", + "repo.desc.sha256": "SHA256", "repo.template.items": "Template-Elemente", "repo.template.git_content": "Git Inhalt (Standardbranch)", "repo.template.git_hooks": "Git-Hooks", "repo.template.git_hooks_tooltip": "Du kannst gerade Git-Hooks nicht ändern oder entfernen, sobald sie hinzugefügt wurden. Wähle das nur aus, wenn du dem Template Repository vertraust.", + "repo.template.webhooks": "Webhooks", "repo.template.topics": "Themen", "repo.template.avatar": "Profilbild", "repo.template.issue_labels": "Issue Label", "repo.template.one_item": "Es muss mindestens ein Template ausgewählt werden", "repo.template.invalid": "Es muss ein Template-Repository ausgewählt werden", + "repo.archive.title": "Dieses Repository ist archiviert. Du kannst Dateien ansehen und es klonen, kannst aber nicht pushen oder Issues/Pull-Requests öffnen.", + "repo.archive.title_date": "Dieses Repository wurde am %s archiviert. Du kannst Dateien ansehen und es klonen, aber nicht pushen oder Issues/Pull-Requests öffnen.", "repo.archive.issue.nocomment": "Dieses Repo ist archiviert. Du kannst Issues nicht kommentieren.", "repo.archive.pull.nocomment": "Dieses Repo ist archiviert. Du kannst Pull-Requests nicht kommentieren.", "repo.form.reach_limit_of_creation_1": "Du hast bereits dein Limit von %d Repository erreicht.", @@ -952,15 +1096,22 @@ "repo.migrate_options_lfs_endpoint.label": "LFS-Endpunkt", "repo.migrate_options_lfs_endpoint.description": "Migration wird versuchen, über den entfernten Git-Server den LFS-Server zu bestimmen. Du kannst auch einen eigenen Endpunkt angeben, wenn die LFS-Dateien woanders gespeichert werden.", "repo.migrate_options_lfs_endpoint.description.local": "Ein lokaler Serverpfad wird ebenfalls unterstützt.", + "repo.migrate_options_lfs_endpoint.placeholder": "Wenn leer gelassen, wird der Endpunkt von der Clone-URL abgeleitet.", "repo.migrate_items": "Migrationselemente", + "repo.migrate_items_wiki": "Wiki", "repo.migrate_items_milestones": "Meilensteine", + "repo.migrate_items_labels": "Labels", + "repo.migrate_items_issues": "Issues", "repo.migrate_items_pullrequests": "Pull-Requests", "repo.migrate_items_merge_requests": "Merge-Requests", + "repo.migrate_items_releases": "Veröffentlichungen", "repo.migrate_repo": "Repository migrieren", "repo.migrate.clone_address": "Migrations- / Klon-URL", "repo.migrate.clone_address_desc": "Die HTTP(S)- oder „git clone“-URL eines bereits existierenden Repositories", + "repo.migrate.github_token_desc": "Du kannst hier ein oder mehrere Token durch Komma getrennt eintippen, um die Migration aufgrund der Github API Ratenlimitierung zu beschleunigen. WARNUNG: Der Missbrauch dieser Funktion kann gegen die Richtlinien des Dienstanbieters verstoßen und zur Kontosperrung führen.", "repo.migrate.clone_local_path": "oder ein lokaler Serverpfad", "repo.migrate.permission_denied": "Du hast keine Berechtigung zum Importieren lokaler Repositories.", + "repo.migrate.permission_denied_blocked": "Du kannst von keinen nicht erlaubten Hosts importieren. Bitte deinen Administrator, die Einstellungen ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS zu überprüfen.", "repo.migrate.invalid_local_path": "Der lokale Pfad ist ungültig. Er existiert nicht oder ist kein Verzeichnis.", "repo.migrate.invalid_lfs_endpoint": "Ungültiger LFS Endpunkt.", "repo.migrate.failed": "Fehler bei der Migration: %v", @@ -968,6 +1119,7 @@ "repo.migrated_from": "Migriert von %[2]s", "repo.migrated_from_fake": "Migriert von %[1]s", "repo.migrate.migrate": "Migrieren von %s", + "repo.migrate.migrating": "Migriere von %s…", "repo.migrate.migrating_failed": "Migrieren von %s fehlgeschlagen.", "repo.migrate.migrating_failed.error": "Migration fehlgeschlagen: %s", "repo.migrate.migrating_failed_no_addr": "Migration fehlgeschlagen.", @@ -980,6 +1132,8 @@ "repo.migrate.codebase.description": "Daten von codebasehq.com migrieren.", "repo.migrate.gitbucket.description": "Daten von GitBucket Instanzen migrieren.", "repo.migrate.codecommit.description": "Daten von AWS CodeCommit migrieren.", + "repo.migrate.codecommit.aws_access_key_id": "AWS Access Key ID", + "repo.migrate.codecommit.aws_secret_access_key": "AWS Secret Access Key", "repo.migrate.codecommit.https_git_credentials_username": "HTTPS-Git-Nutzername", "repo.migrate.codecommit.https_git_credentials_password": "HTTPS-Git-Passwort", "repo.migrate.migrating_git": "Git-Daten werden migriert", @@ -991,6 +1145,7 @@ "repo.migrate.migrating_pulls": "Pull Requests werden migriert", "repo.migrate.cancel_migrating_title": "Migration abbrechen", "repo.migrate.cancel_migrating_confirm": "Möchtest du diese Migration abbrechen?", + "repo.migration_status": "Migrationsstatus", "repo.mirror_from": "Mirror von", "repo.forked_from": "geforkt von", "repo.generated_from": "erzeugt von", @@ -1002,6 +1157,7 @@ "repo.watch": "Beobachten", "repo.unstar": "Favorit entfernen", "repo.star": "Favorisieren", + "repo.fork": "Fork", "repo.action.blocked_user": "Die Aktion kann nicht ausgeführt werden, da du vom Repository-Eigentümer blockiert wurdest.", "repo.download_archive": "Repository herunterladen", "repo.more_operations": "Weitere Operationen", @@ -1010,20 +1166,34 @@ "repo.cite_this_repo": "Dieses Repository zitieren", "repo.create_new_repo_command": "Erstelle ein neues Repository von der Kommandozeile aus", "repo.push_exist_repo": "Bestehendes Repository via Kommandozeile pushen", + "repo.empty_message": "Diese Repository hat noch keinen Inhalt.", "repo.broken_message": "Die Git-Daten, die diesem Repository zugrunde liegen, können nicht gelesen werden. Kontaktiere den Administrator deiner Instanz oder lösche dieses Repository.", + "repo.no_branch": "Dieses Repository hat keine Branches.", + "repo.code": "Code", "repo.code.desc": "Zugriff auf Quellcode, Dateien, Commits und Branches.", + "repo.branch": "Branch", "repo.tree": "Struktur", "repo.clear_ref": "Aktuelle Referenz löschen", "repo.filter_branch_and_tag": "Branch oder Tag filtern", "repo.find_tag": "Tag finden", + "repo.branches": "Branches", + "repo.tags": "Tags", + "repo.issues": "Issues", "repo.pulls": "Pull-Requests", "repo.projects": "Projekte", "repo.packages": "Pakete", + "repo.actions": "Actions", "repo.labels": "Label", + "repo.org_labels_desc": "Labels auf Organisationsebene, die mit allen Repositories in dieser Organisation verwendet werden können", "repo.org_labels_desc_manage": "verwalten", "repo.milestone": "Meilenstein", "repo.milestones": "Meilensteine", + "repo.commits": "Commits", + "repo.commit": "Commit", "repo.release": "Erscheinungsdatum", + "repo.releases": "Releases", + "repo.tag": "Tag", + "repo.git_tag": "Git Tag", "repo.released_this": "hat released", "repo.tagged_this": "hat getaggt", "repo.file.title": "%s in %s", @@ -1032,6 +1202,7 @@ "repo.file_view_source": "Quelltext anzeigen", "repo.file_view_rendered": "Ansicht rendern", "repo.file_view_raw": "Originalformat anzeigen", + "repo.file_permalink": "Permalink", "repo.file_too_large": "Die Datei ist zu groß zum Anzeigen.", "repo.file_is_empty": "Die Datei ist leer.", "repo.code_preview_line_from_to": "Zeilen %[1]d bis %[2]d in %[3]s", @@ -1042,7 +1213,7 @@ "repo.ambiguous_runes_description": "Diese Datei enthält Unicode-Zeichen, die mit anderen Zeichen verwechselt werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.", "repo.invisible_runes_line": "Diese Zeile enthält unsichtbare Unicode-Zeichen", "repo.ambiguous_runes_line": "Diese Zeile enthält mehrdeutige Unicode-Zeichen", - "repo.ambiguous_character": "%[1]c [U+%04[1]X] kann mit %[2]c [U+%04[2]X] verwechselt werden", + "repo.ambiguous_character": "%[1]s könnte mit %[2]s verwechselt werden", "repo.escape_control_characters": "Escapen", "repo.unescape_control_characters": "Unescapen", "repo.file_copy_permalink": "Permalink kopieren", @@ -1061,6 +1232,8 @@ "repo.commit.contained_in": "Dieser Commit ist enthalten in:", "repo.commit.contained_in_default_branch": "Dieser Commit ist im Standard-Branch enthalten", "repo.commit.load_referencing_branches_and_tags": "Lade Branches und Tags, die diesen Commit referenzieren", + "repo.commit.merged_in_pr": "Dieser Commit wurde in Pull Request %s zusammengeführt.", + "repo.blame": "Blame", "repo.download_file": "Datei herunterladen", "repo.normal_view": "Normale Ansicht", "repo.line": "zeile", @@ -1072,14 +1245,19 @@ "repo.editor.edit_file": "Datei bearbeiten", "repo.editor.preview_changes": "Vorschau der Änderungen", "repo.editor.cannot_edit_lfs_files": "LFS-Dateien können im Webinterface nicht bearbeitet werden.", + "repo.editor.cannot_edit_too_large_file": "Diese Datei ist zu groß für die Bearbeitung.", "repo.editor.cannot_edit_non_text_files": "Binärdateien können nicht im Webinterface bearbeitet werden.", + "repo.editor.file_not_editable_hint": "Aber du kannst sie umbennen oder verschieben.", "repo.editor.edit_this_file": "Datei bearbeiten", "repo.editor.this_file_locked": "Datei ist gesperrt", "repo.editor.must_be_on_a_branch": "Du musst dich in einem Branch befinden, um Änderungen an dieser Datei vorzuschlagen oder vorzunehmen.", "repo.editor.fork_before_edit": "Du musst dieses Repository forken, um Änderungen an dieser Datei vorzuschlagen oder vorzunehmen.", "repo.editor.delete_this_file": "Datei löschen", + "repo.editor.delete_this_directory": "Verzeichnis löschen", "repo.editor.must_have_write_access": "Du benötigst Schreibzugriff, um Änderungen an dieser Datei vorzuschlagen oder vorzunehmen.", "repo.editor.file_delete_success": "Datei \"%s\" wurde gelöscht.", + "repo.editor.directory_delete_success": "Verzeichnis \"%s\" wurde gelöscht.", + "repo.editor.delete_directory": "Verzeichnis '%s ' löschen", "repo.editor.name_your_file": "Dateinamen eingeben…", "repo.editor.filename_help": "Füge einen Ordner hinzu, indem du seinen Namen und anschließend '/' eingibst. Entferne einen Ordner indem du die Zurücktaste am Anfang des Feldes drückst.", "repo.editor.or": "oder", @@ -1092,6 +1270,7 @@ "repo.editor.delete": "%s gelöscht", "repo.editor.patch": "Patch anwenden", "repo.editor.patching": "Patche:", + "repo.editor.fail_to_apply_patch": "Patch \"%s\" ist nicht anwendbar", "repo.editor.new_patch": "Neuer Patch", "repo.editor.commit_message_desc": "Eine ausführlichere (optionale) Beschreibung hinzufügen…", "repo.editor.signoff_desc": "Am Ende der Commit Nachricht einen Signed-off-by Anhang vom Committer hinzufügen.", @@ -1104,12 +1283,17 @@ "repo.editor.cancel": "Abbrechen", "repo.editor.filename_cannot_be_empty": "Der Dateiname darf nicht leer sein.", "repo.editor.filename_is_invalid": "Ungültiger Dateiname: \"%s\".", + "repo.editor.commit_email": "Commit E-Mail Adresse", + "repo.editor.invalid_commit_email": "Die E-Mail-Adresse für den Commit ist ungültig.", "repo.editor.branch_does_not_exist": "Branch \"%s\" existiert nicht in diesem Repository.", "repo.editor.branch_already_exists": "Branch \"%s\" existiert bereits in diesem Repository.", "repo.editor.directory_is_a_file": "Der Verzeichnisname \"%s\" wird bereits als Dateiname in diesem Repository verwendet.", + "repo.editor.file_is_a_symlink": "\"%s\" ist ein symbolischer Link. Symbolische Links können mit dem Web-Editor nicht bearbeitet werden.", "repo.editor.filename_is_a_directory": "Der Dateiname \"%s\" wird bereits als Verzeichnisname in diesem Repository verwendet.", + "repo.editor.file_modifying_no_longer_exists": "Die bearbeitete Datei \"%s\" existiert nicht mehr in diesem Repository.", "repo.editor.file_changed_while_editing": "Der Inhalt der Datei hat sich seit dem Beginn der Bearbeitung geändert. Hier klicken, um die Änderungen anzusehen, oder Änderungen erneut comitten, um sie zu überschreiben.", "repo.editor.file_already_exists": "Eine Datei mit dem Namen '%s' existiert bereits in diesem Repository.", + "repo.editor.commit_id_not_matching": "Die Commit-ID stimmt nicht mit der ID überein, bei welcher du mit der Bearbeitung begonnen hast. Committe in einen Patch-Branch und merge daraufhin.", "repo.editor.push_out_of_date": "Der Push scheint veraltet zu sein.", "repo.editor.commit_empty_file_header": "Leere Datei committen", "repo.editor.commit_empty_file_text": "Die Datei, die du commiten willst, ist leer. Fortfahren?", @@ -1127,7 +1311,17 @@ "repo.editor.require_signed_commit": "Branch erfordert einen signierten Commit", "repo.editor.cherry_pick": "Cherry-Picke %s von:", "repo.editor.revert": "%s zurücksetzen auf:", + "repo.editor.failed_to_commit": "Fehler beim Übertragen der Änderungen.", + "repo.editor.failed_to_commit_summary": "Fehlermeldung:", + "repo.editor.fork_create": "Forke das Repository, um Änderungen vorzuschlagen", + "repo.editor.fork_create_description": "Sie können dieses Repository nicht direkt bearbeiten. Stattdessen können Sie einen Fork erstellen, Änderungen vornehmen und einen Pull-Request erstellen.", + "repo.editor.fork_edit_description": "Sie können dieses Repository nicht direkt bearbeiten. Die Änderungen werden in Ihren Fork %sgeschrieben, so dass Sie einen Pull-Request erstellen können.", + "repo.editor.fork_not_editable": "Sie haben dieses Repository abgespalten (forked), aber Ihr Fork ist nicht editierbar.", + "repo.editor.fork_failed_to_push_branch": "Konnte Branch %s nicht ans Repository senden.", + "repo.editor.fork_branch_exists": "Branch \"%s\" existiert bereits in Ihrem Fork. Bitte wählen Sie einen neuen Branch-Namen.", "repo.commits.desc": "Durchsuche die Quellcode-Änderungshistorie.", + "repo.commits.commits": "Commits", + "repo.commits.history_enable_follow_renames": "Umbenennungen einschließen", "repo.commits.no_commits": "Keine gemeinsamen Commits. \"%s\" und \"%s\" haben vollständig unterschiedliche Historien.", "repo.commits.nothing_to_compare": "Diese Branches sind auf demselben Stand.", "repo.commits.search.tooltip": "Du kannst Suchbegriffen \"author:\", \" committer:\", \"after:\", oder \" before:\" voranstellen, z.B. \"revert author:Alice before:2019-04-01\".", @@ -1161,10 +1355,13 @@ "repo.projects.desc": "Verwalte Issues und Pull-Requests in Projekten.", "repo.projects.description": "Beschreibung (optional)", "repo.projects.description_placeholder": "Beschreibung", + "repo.projects.empty": "Noch keine Projekte.", + "repo.projects.empty_description": "Erstellen Sie ein Projekt, um Issues und Pull-Requests zu koordinieren.", "repo.projects.create": "Projekt erstellen", "repo.projects.title": "Titel", "repo.projects.new": "Neues Projekt", "repo.projects.new_subheader": "Koordiniere, verfolge und aktualisiere deine Arbeit an einem Ort, so dass Projekte transparent und planmäßig bleiben.", + "repo.projects.no_results": "Keine Projekte entsprechen Ihrer Suche.", "repo.projects.create_success": "Das Projekt \"%s\" wurde erstellt.", "repo.projects.deletion": "Projekt löschen", "repo.projects.deletion_desc": "Das Löschen eines Projekts entfernt es von allen damit zusammenhängenden Issues. Fortfahren?", @@ -1175,16 +1372,21 @@ "repo.projects.edit_success": "Projekt \"%s\" wurde aktualisiert.", "repo.projects.type.none": "Ohne", "repo.projects.type.basic_kanban": "Einfaches Kanban", + "repo.projects.type.bug_triage": "Bug Triage", "repo.projects.template.desc": "Projektvorlage", "repo.projects.template.desc_helper": "Wähle eine Projektvorlage aus, um loszulegen", "repo.projects.column.edit": "Spalte bearbeiten", + "repo.projects.column.edit_title": "Name", + "repo.projects.column.new_title": "Name", "repo.projects.column.new_submit": "Spalte erstellen", "repo.projects.column.new": "Neue Spalte", "repo.projects.column.set_default": "Als Standard verwenden", "repo.projects.column.set_default_desc": "Diese Spalte als Standard für unkategorisierte Issues und Pull Requests festlegen", + "repo.projects.column.default_column_hint": "Neue Tickets für dieses Projekt werden zu dieser Spalte hinzugefügt", "repo.projects.column.delete": "Spalte löschen", "repo.projects.column.deletion_desc": "Beim Löschen einer Projektspalte werden alle Einträge in die Standard-Spalte verschoben. Fortfahren?", "repo.projects.column.color": "Farbe", + "repo.projects.column": "Spalte", "repo.projects.open": "Öffnen", "repo.projects.close": "Schließen", "repo.projects.column.assigned_to": "Zugewiesen an", @@ -1202,11 +1404,12 @@ "repo.issues.new": "Neues Issue", "repo.issues.new.title_empty": "Der Titel kann nicht leer sein", "repo.issues.new.labels": "Label", - "repo.issues.new.no_label": "Kein Label", + "repo.issues.new.no_labels": "Keine labels", "repo.issues.new.clear_labels": "Label entfernen", "repo.issues.new.projects": "Projekte", "repo.issues.new.clear_projects": "Projekte löschen", "repo.issues.new.no_projects": "Kein Projekt", + "repo.issues.new.no_column": "Keine Spalte", "repo.issues.new.open_projects": "Offene Projekte", "repo.issues.new.closed_projects": "Geschlossene Projekte", "repo.issues.new.no_items": "Keine Einträge", @@ -1218,6 +1421,7 @@ "repo.issues.new.no_assignees": "Niemand zuständig", "repo.issues.new.no_reviewers": "Keine Reviewer", "repo.issues.new.blocked_user": "Das Issue kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest.", + "repo.issues.edit.already_changed": "Änderungen zum Issue konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisiere die Seite und bearbeite diese erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden.", "repo.issues.edit.blocked_user": "Der Inhalt kann nicht bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest.", "repo.issues.choose.get_started": "Los geht's", "repo.issues.choose.open_external_link": "Öffnen", @@ -1260,6 +1464,7 @@ "repo.issues.remove_ref_at": "hat die Referenz %s entfernt %s", "repo.issues.add_ref_at": "hat die Referenz %s hinzugefügt %s", "repo.issues.delete_branch_at": "löschte die Branch %s %s", + "repo.issues.filter_label": "Label", "repo.issues.filter_label_exclude": "„Alt + Klick/Enter verwenden, um Label auszuschließen”", "repo.issues.filter_label_no_select": "Alle Label", "repo.issues.filter_label_select_no_label": "Kein Label", @@ -1288,6 +1493,7 @@ "repo.issues.filter_sort": "Sortieren", "repo.issues.filter_sort.latest": "Neueste", "repo.issues.filter_sort.oldest": "Älteste", + "repo.issues.filter_sort.recentupdate": "Zuletzt aktualisiert", "repo.issues.filter_sort.leastupdate": "Am Längsten nicht aktualisiert", "repo.issues.filter_sort.mostcomment": "Am meisten kommentiert", "repo.issues.filter_sort.leastcomment": "Am wenigsten kommentiert", @@ -1297,8 +1503,10 @@ "repo.issues.filter_sort.feweststars": "Wenigste Favoriten", "repo.issues.filter_sort.mostforks": "Meiste Forks", "repo.issues.filter_sort.fewestforks": "Wenigste Forks", + "repo.issues.quick_goto": "Gehe zu Issue", "repo.issues.action_open": "Öffnen", "repo.issues.action_close": "Schließen", + "repo.issues.action_label": "Label", "repo.issues.action_milestone": "Meilenstein", "repo.issues.action_milestone_no_select": "Kein Meilenstein", "repo.issues.action_assignee": "Zuständig", @@ -1321,11 +1529,13 @@ "repo.issues.commented_at": "hat %s kommentiert", "repo.issues.delete_comment_confirm": "Bist du sicher dass du diesen Kommentar löschen möchtest?", "repo.issues.context.copy_link": "Link kopieren", + "repo.issues.context.copy_source": "Quelle kopieren", "repo.issues.context.quote_reply": "Antwort zitieren", "repo.issues.context.reference_issue": "In neuem Issue referenzieren", "repo.issues.context.edit": "Bearbeiten", "repo.issues.context.delete": "Löschen", "repo.issues.no_content": "Keine Beschreibung angegeben.", + "repo.issues.comment_no_content": "Kein Kommentar vorhanden.", "repo.issues.close": "Issue schließen", "repo.issues.comment_pull_merged_at": "hat Commit %[1]s in %[2]s %[3]s gemerged", "repo.issues.comment_manually_pull_merged_at": "hat Commit %[1]s in %[2]s %[3]s manuell gemerged", @@ -1408,6 +1618,7 @@ "repo.issues.unlock_comment": "hat diese Diskussion %s entsperrt", "repo.issues.lock_confirm": "Sperren", "repo.issues.unlock_confirm": "Entsperren", + "repo.issues.lock.notice_1": "- Andere Benutzer können diesem Problem keine neuen Kommentare hinzufügen.", "repo.issues.lock.notice_2": "- Du und andere Mitarbeiter mit Zugriff auf dieses Repository können weiterhin für andere sichtbare Kommentare hinterlassen.", "repo.issues.lock.notice_3": "- Du kannst die Diskussion jederzeit wieder entsperren.", "repo.issues.unlock.notice_1": "- Jeder wird wieder in der Lage sein, zu diesem Issue zu kommentieren.", @@ -1431,6 +1642,8 @@ "repo.issues.time_estimate_invalid": "Format der Zeitschätzung ist ungültig", "repo.issues.start_tracking_history": "hat die Zeiterfassung %s gestartet", "repo.issues.tracker_auto_close": "Der Timer wird automatisch gestoppt, wenn dieser Issue geschlossen wird", + "repo.issues.stopwatch_already_stopped": "Der Timer für dieses Problem ist bereits beendet", + "repo.issues.stopwatch_already_created": "Der Timer für dieses Issue existiert bereits", "repo.issues.tracking_already_started": "Du hast die Zeiterfassung bereits in diesem Issue gestartet!", "repo.issues.stop_tracking": "Timer stoppen", "repo.issues.stop_tracking_history": "hat für %[1]s %[2]s gearbeitet", @@ -1457,9 +1670,10 @@ "repo.issues.due_date_form_add": "Fälligkeitsdatum hinzufügen", "repo.issues.due_date_form_edit": "Bearbeiten", "repo.issues.due_date_form_remove": "Entfernen", + "repo.issues.due_date_not_writer": "Du musst Schreibrechte für dieses Repository haben, um das Fälligkeitsdatum zu ändern.", "repo.issues.due_date_not_set": "Kein Fälligkeitsdatum gesetzt.", "repo.issues.due_date_added": "hat %[2]s das Fälligkeitsdatum %[1]s hinzugefügt", - "repo.issues.due_date_modified": "ändert das Abgabedatum von %[2]s auf %[1]s %[3]s s", + "repo.issues.due_date_modified": "ändert das Abgabedatum von %[2]s auf %[1]s %[3]s", "repo.issues.due_date_remove": "hat %[2]s das Fälligkeitsdatum %[1]s entfernt", "repo.issues.due_date_overdue": "Überfällig", "repo.issues.due_date_invalid": "Das Fälligkeitsdatum ist ungültig oder außerhalb des zulässigen Bereichs. Bitte verwende das Format „jjjj-mm-tt“.", @@ -1479,7 +1693,9 @@ "repo.issues.dependency.issue_closing_blockedby": "Das Schließen dieses Issues wird von den folgenden Issues blockiert", "repo.issues.dependency.issue_close_blocks": "Dieses Issue blockiert die Schließung der folgenden Issues", "repo.issues.dependency.pr_close_blocks": "Dieser Pull-Request blockiert die Schließung der folgenden Issues", + "repo.issues.dependency.issue_close_blocked": "Du musst alle Issues, die dieses Issue blockieren, schließen, bevor du es schließen kannst.", "repo.issues.dependency.issue_batch_close_blocked": "Das Schließen der ausgewählten Issues ist nicht möglich, da das Issue #%d noch offene Abhängigkeiten hat", + "repo.issues.dependency.pr_close_blocked": "Du musst alle Issues, die diesen Pull-Request blockieren, schließen, bevor du ihn mergen kannst.", "repo.issues.dependency.blocks_short": "Blockiert", "repo.issues.dependency.blocked_by_short": "Abhängig von", "repo.issues.dependency.remove_header": "Abhängigkeit löschen", @@ -1490,20 +1706,25 @@ "repo.issues.dependency.add_error_dep_issue_not_exist": "Abhängiges Issue existiert nicht.", "repo.issues.dependency.add_error_dep_not_exist": "Abhängigkeit existiert nicht.", "repo.issues.dependency.add_error_dep_exists": "Abhängigkeit existiert bereits.", + "repo.issues.dependency.add_error_cannot_create_circular": "Du kannst keine Abhängigkeit erstellen, bei welcher sich zwei Issues gegenseitig blockieren.", "repo.issues.dependency.add_error_dep_not_same_repo": "Beide Issues müssen sich im selben Repository befinden.", "repo.issues.review.self.approval": "Du kannst nicht dein eigenen Pull-Request genehmigen.", "repo.issues.review.self.rejection": "Du kannst keine Änderungen an deinem eigenen Pull-Request anfragen.", "repo.issues.review.approve": "hat die Änderungen %s genehmigt", "repo.issues.review.comment": "hat %s überprüft", + "repo.issues.review.dismissed": "verwarf %ss Review %s", "repo.issues.review.dismissed_label": "Verworfen", "repo.issues.review.left_comment": "hat einen Kommentar hinterlassen", "repo.issues.review.content.empty": "Du musst einen Kommentar hinterlassen, der die gewünschte(n) Änderung(en) beschreibt.", "repo.issues.review.reject": "hat %s Änderungen angefragt", "repo.issues.review.wait": "wurde für ein Review %s angefragt", + "repo.issues.review.codeowners_rules": "CODEOWNER Regeln", "repo.issues.review.add_review_request": "hat ein Review von %s %s angefragt", "repo.issues.review.remove_review_request": "hat die Aufforderung zum Review an %s %s entfernt", + "repo.issues.review.remove_review_request_self": "hat das Review verweigert %s", "repo.issues.review.pending": "Ausstehend", "repo.issues.review.pending.tooltip": "Dieser Kommentar ist derzeit nicht für andere Benutzer sichtbar. Um deine ausstehenden Kommentare einzureichen, wähle \"%s\" -> \"%s/%s/%s\" oben auf der Seite.", + "repo.issues.review.review": "Überprüfen", "repo.issues.review.reviewers": "Reviewer", "repo.issues.review.outdated": "Veraltet", "repo.issues.review.outdated_description": "Der Inhalt hat sich geändert, seit dieser Kommentar abgegeben wurde", @@ -1522,6 +1743,7 @@ "repo.issues.review.rejected": "Änderungen angefordert", "repo.issues.review.stale": "Aktualisiert seit der Genehmigung", "repo.issues.review.unofficial": "Ungezählte Genehmigung", + "repo.issues.assignee.error": "Aufgrund eines unerwarteten Fehlers konnten nicht alle Beauftragten hinzugefügt werden.", "repo.issues.reference_issue.body": "Beschreibung", "repo.issues.content_history.deleted": "gelöscht", "repo.issues.content_history.edited": "bearbeitet", @@ -1532,16 +1754,20 @@ "repo.issues.reference_link": "Referenz: %s", "repo.compare.compare_base": "Basis", "repo.compare.compare_head": "vergleichen", + "repo.compare.title": "Änderungen vergleichen", + "repo.compare.description": "Wählen Sie zwei Branches oder Tags aus, um zu sehen, was sich geändert hat, oder um eine neue Pull Request zu starten.", "repo.pulls.desc": "Pull-Requests und Code-Reviews aktivieren.", "repo.pulls.new": "Neuer Pull-Request", + "repo.pulls.new.description": "Diskutieren und überprüfen Sie die Änderungen in diesem Review mit anderen.", "repo.pulls.new.blocked_user": "Der Pull Request kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest.", "repo.pulls.new.must_collaborator": "Du musst Mitarbeiter sein, um Pull-Requests zu erstellen.", + "repo.pulls.new.already_existed": "Zwischen diesen Branch existiert bereits ein Pull-Request", + "repo.pulls.edit.already_changed": "Änderungen zum Pull Request konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisiere die Seite und bearbeite diesen erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden.", "repo.pulls.view": "Pull-Request ansehen", "repo.pulls.compare_changes": "Neuer Pull-Request", "repo.pulls.allow_edits_from_maintainers": "Änderungen von Maintainern erlauben", "repo.pulls.allow_edits_from_maintainers_desc": "Nutzer mit Schreibzugriff auf den Basisbranch können auch auf diesen Branch pushen", "repo.pulls.allow_edits_from_maintainers_err": "Aktualisieren fehlgeschlagen", - "repo.pulls.compare_changes_desc": "Wähle den Ziel- und Quellbranch aus.", "repo.pulls.has_viewed_file": "Gesehen", "repo.pulls.has_changed_since_last_review": "Inzwischen geändert", "repo.pulls.viewed_files_label": "%[1]d / %[2]d Dateien reviewed", @@ -1556,16 +1782,22 @@ "repo.pulls.show_changes_since_your_last_review": "Zeige Änderungen seit deinem letzten Review", "repo.pulls.showing_only_single_commit": "Nur Änderungen aus Commit %[1]s werden angezeigt", "repo.pulls.showing_specified_commit_range": "Zeige nur die Änderungen zwischen %[1]s..%[2]s", + "repo.pulls.select_commit_hold_shift_for_range": "Commit auswählen. Halte Shift und klicke auf den Bereich um diesen auszuwählen.", "repo.pulls.review_only_possible_for_full_diff": "Ein Review ist nur möglich, wenn das vollständige Diff angezeigt wird", "repo.pulls.filter_changes_by_commit": "Nach Commit filtern", "repo.pulls.nothing_to_compare": "Diese Branches sind identisch. Es muss kein Pull-Request erstellt werden.", + "repo.pulls.no_common_history": "Diese Branches teilen keine gemeinsame Merge Basis. Wähle eine andere Basis oder vergleiche einen anderen Branch.", + "repo.pulls.nothing_to_compare_have_tag": "Die ausgewählten Branches/Tags sind gleich.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "Diese Branches sind gleich. Der Pull-Request wird leer sein.", "repo.pulls.has_pull_request": "Es existiert bereits ein Pull-Request zwischen diesen beiden Branches: %[2]s#%[3]d", "repo.pulls.create": "Pull-Request erstellen", "repo.pulls.title_desc": "möchte %[1]d Commits von %[2]s nach %[3]s mergen", "repo.pulls.merged_title_desc": "hat %[1]d Commits von %[2]s nach %[3]s %[4]s zusammengeführt", "repo.pulls.change_target_branch_at": "hat den Zielbranch von %s nach %s %s geändert", + "repo.pulls.marked_as_work_in_progress_at": "Markiere den Pull Request als in Arbeit befindliche %s", + "repo.pulls.marked_as_ready_for_review_at": "Markiere den Pull -Request als bereit zur Überprüfung %s", "repo.pulls.tab_conversation": "Diskussion", + "repo.pulls.tab_commits": "Commits", "repo.pulls.tab_files": "Geänderte Dateien", "repo.pulls.reopen_to_merge": "Bitte diesen Pull-Request wieder öffnen, um zu mergen.", "repo.pulls.cant_reopen_deleted_branch": "Dieser Pull-Request kann nicht wieder geöffnet werden, da die Branch bereits gelöscht wurde.", @@ -1582,11 +1814,14 @@ "repo.pulls.remove_prefix": "%s Präfix entfernen", "repo.pulls.data_broken": "Dieser Pull-Requests ist kaputt, da Fork-Informationen gelöscht wurden.", "repo.pulls.files_conflicted": "Dieser Pull-Request hat Änderungen, die im Widerspruch zum Ziel-Branch stehen.", + "repo.pulls.files_conflicted_no_listed_files": "(Keine Konfliktdateien aufgeführt)", + "repo.pulls.is_checking": "Prüfe auf Merge Konflikte…", "repo.pulls.is_ancestor": "Dieser Branch ist bereits im Zielbranch enthalten. Es gibt nichts zu mergen.", "repo.pulls.is_empty": "Die Änderungen an diesem Branch sind bereits auf dem Zielbranch. Dies wird ein leerer Commit sein.", "repo.pulls.required_status_check_failed": "Einige erforderliche Prüfungen waren nicht erfolgreich.", "repo.pulls.required_status_check_missing": "Einige erforderliche Prüfungen fehlen.", "repo.pulls.required_status_check_administrator": "Als Administrator kannst du diesen Pull-Request weiterhin mergen.", + "repo.pulls.required_status_check_bypass_allowlist": "Du darfst die Schutzregeln für diesen Merge umgehen.", "repo.pulls.blocked_by_approvals": "Dieser Pull-Request hat noch nicht genügend Genehmigungen. %d von %d Genehmigungen erteilt.", "repo.pulls.blocked_by_approvals_whitelisted": "Dieser Pull-Request hat noch nicht genug erforderliche Genehmigungen. %d von %d Genehmigungen von Benutzern oder Teams auf der Berechtigungsliste.", "repo.pulls.blocked_by_rejection": "Dieser Pull-Request hat Änderungen, die von einem offiziellen Reviewer angefragt wurden.", @@ -1605,43 +1840,64 @@ "repo.pulls.reject_count_n": "%d Änderungsanfragen", "repo.pulls.waiting_count_1": "%d wartendes Review", "repo.pulls.waiting_count_n": "%d wartende Reviews", + "repo.pulls.wrong_commit_id": "Commit-ID muss eine Commit-ID im Ziel Branch sein", "repo.pulls.no_merge_desc": "Dieser Pull-Request kann nicht gemerged werden, da keine Mergeoptionen aktiviert sind.", "repo.pulls.no_merge_helper": "Aktiviere Mergeoptionen in den Repositoryeinstellungen oder merge den Pull-Request manuell.", "repo.pulls.no_merge_wip": "Dieser Pull Request kann nicht gemergt werden, da er als Work In Progress gekennzeichnet ist.", + "repo.pulls.no_merge_not_ready": "Dieser Pull-Request kann nicht gemergt werden, überprüfe den Reviewstatus und die Statusprüfungen.", "repo.pulls.no_merge_access": "Du bist nicht berechtigt, diesen Pull-Request zu mergen.", "repo.pulls.merge_pull_request": "Merge Commit erstellen", + "repo.pulls.rebase_merge_pull_request": "Rebase und dann fast-forward", + "repo.pulls.rebase_merge_commit_pull_request": "Rebase, dann Merge Commit erstellen", "repo.pulls.squash_merge_pull_request": "Squash Commit erstellen", "repo.pulls.fast_forward_only_merge_pull_request": "Nur Fast-forward", "repo.pulls.merge_manually": "Manuell mergen", "repo.pulls.merge_commit_id": "Der Mergecommit ID", "repo.pulls.require_signed_wont_sign": "Der Branch erfordert einen signierten Commit, aber dieser Merge wird nicht signiert", + "repo.pulls.require_signed_head_commits_unverified": "Der Branch erfordert signierte Commits aber ein oder mehrere Commits für diesen Pull Request sind nicht verifiziert", "repo.pulls.invalid_merge_option": "Du kannst diese Mergeoption auf diesen Pull-Request nicht anwenden.", + "repo.pulls.merge_conflict": "Merge fehlgeschlagen: Beim Mergen gab es einen Konflikt. Hinweis: Probiere eine andere Strategie.", "repo.pulls.merge_conflict_summary": "Fehlermeldung", + "repo.pulls.rebase_conflict": "Merge fehlgeschlagen: Es gab einen Konflikt beim Rebasen des Commits: %[1]s. Hinweis: Versuche eine andere Strategie.", "repo.pulls.rebase_conflict_summary": "Fehlermeldung", + "repo.pulls.unrelated_histories": "Merge fehlgeschlagen: Der Head des Merges und die Basis haben keinen gemeinsamen Verlauf. Hinweis: Versuche eine andere Strategie.", + "repo.pulls.merge_out_of_date": "Merge fehlgeschlagen: Während des Mergens wurde die Basis aktualisiert. Hinweis: Versuche es erneut.", + "repo.pulls.head_out_of_date": "Merge fehlgeschlagen: Während des Merges wurde der Head aktualisiert. Hinweis: Versuchen Sie es erneut.", + "repo.pulls.has_merged": "Fehler: Der Pull-Request wurde gemerged, du kannst den Zielbranch nicht wieder mergen oder ändern.", "repo.pulls.push_rejected": "Push fehlgeschlagen: Der Push wurde abgelehnt. Überprüfe die Git Hooks für dieses Repository.", "repo.pulls.push_rejected_summary": "Vollständige Ablehnungsmeldung", + "repo.pulls.push_rejected_no_message": "Push fehlgeschlagen: Der Push wurde abgelehnt, aber es gab keine Fehlermeldung. Überprüfe die Git Hooks für dieses Repository.", "repo.pulls.open_unmerged_pull_exists": "Du kannst diesen Pull-Request nicht erneut öffnen, da noch ein anderer (#%d) mit identischen Eigenschaften offen ist.", "repo.pulls.status_checking": "Einige Prüfungen sind noch ausstehend", "repo.pulls.status_checks_success": "Alle Prüfungen waren erfolgreich", "repo.pulls.status_checks_warning": "Einige Prüfungen meldeten Warnungen", - "repo.pulls.status_checks_failure": "Einige Prüfungen sind fehlgeschlagen", + "repo.pulls.status_checks_failure_required": "Einige erforderliche Prüfungen sind fehlgeschlagen", + "repo.pulls.status_checks_failure_optional": "Einige optionale Prüfungen sind fehlgeschlagen", "repo.pulls.status_checks_error": "Einige Checks meldeten Fehler", "repo.pulls.status_checks_requested": "Erforderlich", + "repo.pulls.status_checks_details": "Details", "repo.pulls.status_checks_hide_all": "Alle Prüfungen ausblenden", "repo.pulls.status_checks_show_all": "Alle Prüfungen anzeigen", + "repo.pulls.status_checks_approve_all": "Alle Workflows genehmigen", + "repo.pulls.status_checks_need_approvals": "%d Workflow(s) wartet/n auf Genehmigung", + "repo.pulls.status_checks_need_approvals_helper": "Der Workflow läuft erst nach Genehmigung des Repository Betreuers.", "repo.pulls.update_branch": "Branch durch Mergen aktualisieren", "repo.pulls.update_branch_rebase": "Branch durch Rebase aktualisieren", "repo.pulls.update_branch_success": "Branch-Aktualisierung erfolgreich", "repo.pulls.update_not_allowed": "Du hast keine Berechtigung, die Branch zu Updaten", "repo.pulls.outdated_with_base_branch": "Dieser Branch enthält nicht die neusten Commits der Basis-Branch", "repo.pulls.close": "Pull-Request schließen", + "repo.pulls.reopen": "Pull Request erneut öffnen", "repo.pulls.closed_at": "hat diesen Pull-Request %[2]s geschlossen", "repo.pulls.reopened_at": "hat diesen Pull-Request %[2]s wieder geöffnet", "repo.pulls.cmd_instruction_hint": "Zeige Kommandozeilenanweisungen", + "repo.pulls.cmd_instruction_checkout_title": "Checkout", "repo.pulls.cmd_instruction_checkout_desc": "Wechsle auf einen neuen Branch in deinem lokalen Repository und teste die Änderungen.", "repo.pulls.cmd_instruction_merge_title": "Mergen", "repo.pulls.cmd_instruction_merge_desc": "Die Änderungen mergen und auf Gitea aktualisieren.", + "repo.pulls.cmd_instruction_merge_warning": "Warnung: Dieser Vorgang kann den Pull Request nicht zusammenführen, da \"manuelle Zusammenführung\" nicht aktiviert ist.", "repo.pulls.clear_merge_message": "Merge-Nachricht löschen", + "repo.pulls.clear_merge_message_hint": "Das Löschen der Merge Nachricht wird nur den Inhalt der Commit-Nachricht entfernen und generierte Git-Trailer wie \"Co-Authored-By…\" erhalten.", "repo.pulls.auto_merge_button_when_succeed": "(Wenn die Checks erfolgreich sind)", "repo.pulls.auto_merge_when_succeed": "Automergen, sobald alle Checks erfüllt sind", "repo.pulls.auto_merge_newly_scheduled": "Der Pull-Request wird automatisch gemergt, wenn alle Checks erfolgreich sind.", @@ -1685,6 +1941,7 @@ "repo.milestones.deletion": "Meilenstein löschen", "repo.milestones.deletion_desc": "Das Löschen des Meilensteins entfernt ihn von allen Issues. Fortfahren?", "repo.milestones.deletion_success": "Der Meilenstein wurde gelöscht.", + "repo.milestones.filter_sort.name": "Name", "repo.milestones.filter_sort.earliest_due_data": "Frühestes Fälligkeitsdatum", "repo.milestones.filter_sort.latest_due_date": "Spätestes Fälligkeitsdatum", "repo.milestones.filter_sort.least_complete": "Am wenigsten vollständig", @@ -1692,18 +1949,20 @@ "repo.milestones.filter_sort.most_issues": "Meiste Issues", "repo.milestones.filter_sort.least_issues": "Wenigste Issues", "repo.signing.will_sign": "Dieser Commit wird mit dem Key \"%s\" signiert werden.", + "repo.signing.wont_sign.error": "Es gab einen Fehler bei der Prüfung, ob der Commit signiert werden kann.", "repo.signing.wont_sign.nokey": "Es ist kein Schlüssel zum Signieren dieses Commits verfügbar.", "repo.signing.wont_sign.never": "Commits werden nie signiert.", "repo.signing.wont_sign.always": "Commits werden immer signiert.", "repo.signing.wont_sign.pubkey": "Der Commit wird nicht signiert, da du keinen öffentlichen Schlüssel mit deinem Account verknüpft hast.", + "repo.signing.wont_sign.twofa": "Du musst Zwei-Faktor-Authentifizierung aktivieren, damit Commits signiert werden.", "repo.signing.wont_sign.parentsigned": "Der Commit wird nicht signiert werden, da der vorherige Commit nicht signiert ist.", "repo.signing.wont_sign.basesigned": "Der Merge Commit wird nicht signiert werden, da der Basis-Commit nicht signiert ist.", "repo.signing.wont_sign.headsigned": "Der Merge Commit wird nicht signiert werden, da der Head-Commit nicht signiert ist.", "repo.signing.wont_sign.commitssigned": "Der Merge Commit wird nicht signiert werden, da alle zugehörigen Commits nicht signiert sind.", "repo.signing.wont_sign.approved": "Der Merge Commit wird nicht signiert werden, da der Pull Request nicht genehmigt wurde.", - "repo.signing.wont_sign.not_signed_in": "Du bist nicht eingeloggt.", "repo.ext_wiki": "Zugriff auf externes Wiki", "repo.ext_wiki.desc": "Verweis auf externes Wiki.", + "repo.wiki": "Wiki", "repo.wiki.welcome": "Willkommen im Wiki.", "repo.wiki.welcome_desc": "Im Wiki kannst du Dokumentation schreiben und sie mit Mitarbeitern teilen.", "repo.wiki.desc": "Schreibe und teile Dokumentation mit Mitarbeitern.", @@ -1761,6 +2020,8 @@ "repo.activity.active_issues_count_n": "%d aktive Issues", "repo.activity.closed_issues_count_1": "Geschlossenes Issue", "repo.activity.closed_issues_count_n": "Geschlossene Issues", + "repo.activity.title.issues_1": "%d Issue", + "repo.activity.title.issues_n": "%d Issues", "repo.activity.title.issues_closed_from": "%s von %s geschlossen", "repo.activity.title.issues_created_by": "%s von %s erstellt", "repo.activity.closed_issue_label": "Geschlossen", @@ -1771,8 +2032,11 @@ "repo.activity.title.unresolved_conv_n": "%d offene Konversationen", "repo.activity.unresolved_conv_desc": "Diese kürzlich geänderten Issues und Pull-Requests wurden noch nicht gelöst.", "repo.activity.unresolved_conv_label": "Offen", + "repo.activity.title.releases_1": "%d Release", + "repo.activity.title.releases_n": "%d Releases", "repo.activity.title.releases_published_by": "%s von %s veröffentlicht", "repo.activity.published_release_label": "Veröffentlicht", + "repo.activity.no_git_activity": "In diesem Zeitraum sind keine Commit-Aktivität vorhanden.", "repo.activity.git_stats_exclude_merges": "Merges ausgenommen,", "repo.activity.git_stats_author_1": "%d Autor", "repo.activity.git_stats_author_n": "%d Autoren", @@ -1794,19 +2058,26 @@ "repo.activity.git_stats_deletion_1": "%d Löschung", "repo.activity.git_stats_deletion_n": "%d Löschungen", "repo.contributors.contribution_type.filter_label": "Beitragstyp:", + "repo.contributors.contribution_type.commits": "Commits", "repo.contributors.contribution_type.additions": "Ergänzungen", "repo.contributors.contribution_type.deletions": "Löschungen", "repo.settings": "Einstellungen", + "repo.settings.desc": "In den Einstellungen kannst du die Einstellungen des Repositories anpassen.", + "repo.settings.options": "Repository", "repo.settings.public_access": "Öffentlicher Zugriff", "repo.settings.public_access_desc": "Konfiguriere die Zugriffsrechte öffentlicher Besucher, um die Standardwerte dieses Repositorys zu überschreiben.", "repo.settings.public_access.docs.not_set": "Nicht gesetzt: keine zusätzliche öffentliche Zugriffsberechtigung. Die Berechtigung der Besucher folgt den Sichtbarkeits- und Mitgliedsberechtigungen des Repositorys.", "repo.settings.public_access.docs.anonymous_read": "Anonymes Lesen: Nicht angemeldete Benutzer können mit Leseberechtigung auf die Einheit zugreifen.", + "repo.settings.public_access.docs.everyone_read": "Alle Lesen: Alle angemeldeten Benutzer können mit Leseberechtigung auf die Einheit zugreifen. Leseberechtigung für Issues/Pull-Request-Einheiten bedeutet auch, dass Benutzer neue Issues/Pull-Requests erstellen können.", "repo.settings.public_access.docs.everyone_write": "Alle Schreiben: Alle angemeldeten Benutzer haben Schreibrechte auf die Einheit. Nur die Wiki-Einheit unterstützt diese Berechtigung.", "repo.settings.collaboration": "Mitarbeiter", + "repo.settings.collaboration.admin": "Administrator", "repo.settings.collaboration.write": "Schreibrechte", "repo.settings.collaboration.read": "Leserechte", "repo.settings.collaboration.owner": "Besitzer", "repo.settings.collaboration.undefined": "Nicht definiert", + "repo.settings.collaboration.per_unit": "Berechtigungen", + "repo.settings.hooks": "Webhooks", "repo.settings.githooks": "Git-Hooks", "repo.settings.basic_settings": "Grundeinstellungen", "repo.settings.mirror_settings": "Mirror-Einstellungen", @@ -1815,6 +2086,7 @@ "repo.settings.mirror_settings.docs.disabled_push_mirror.instructions": "Richte Dein Repository so ein, dass es automatisch Commits, Tags und Branches aus einem anderen Repository pullen kann.", "repo.settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning": "Im Moment ist dies nur im Menü \"Neue Migration\" möglich. Für weitere Informationen konsultiere bitte:", "repo.settings.mirror_settings.docs.disabled_push_mirror.info": "Push-Mirrors wurden von Ihrem Administrator deaktiviert.", + "repo.settings.mirror_settings.docs.no_new_mirrors": "Dein Repository spiegelt Änderungen von oder zu einem anderen Repository. Bitte beachte, dass du gerade keine neuen Mirror anlegen kannst.", "repo.settings.mirror_settings.docs.can_still_use": "Obwohl du existierende Mirrors gerade nicht bearbeiten oder neu anlegen kannst, sind bestehende Mirrors weiterhin nutzbar.", "repo.settings.mirror_settings.docs.pull_mirror_instructions": "Um einen Pull-Mirror einzurichten, konsultiere bitte:", "repo.settings.mirror_settings.docs.more_information_if_disabled": "Hier kannst du mehr über Push- und Pull-Mirrors erfahren:", @@ -1824,6 +2096,8 @@ "repo.settings.mirror_settings.mirrored_repository": "Gespiegeltes Repository", "repo.settings.mirror_settings.pushed_repository": "Gepushtes Repository", "repo.settings.mirror_settings.direction": "Richtung", + "repo.settings.mirror_settings.direction.pull": "Pull", + "repo.settings.mirror_settings.direction.push": "Push", "repo.settings.mirror_settings.last_update": "Letzte Aktualisierung", "repo.settings.mirror_settings.push_mirror.none": "Keine Push-Mirrors konfiguriert", "repo.settings.mirror_settings.push_mirror.remote_url": "URL zum Git-Remote-Repository", @@ -1867,7 +2141,11 @@ "repo.settings.pulls_desc": "Repository-Pull-Requests aktivieren", "repo.settings.pulls.ignore_whitespace": "Bei Konflikten Leerzeichen ignorieren", "repo.settings.pulls.enable_autodetect_manual_merge": "Autoerkennung von manuellen Merges aktivieren (in Ausnahmefällen können Fehleinschätzungen auftreten)", + "repo.settings.pulls.allow_merge_update": "Update von Pull Request Branches per Merge erlauben", "repo.settings.pulls.allow_rebase_update": "Update von Pull Request Branches per Rebase erlauben", + "repo.settings.pulls.default_update_style": "Standard Branch Update Stil", + "repo.settings.pulls.default_target_branch": "Standard Ziel Branch für neue Pull Requests", + "repo.settings.pulls.default_target_branch_default": "Standard Branch (%s)", "repo.settings.pulls.default_delete_branch_after_merge": "Standardmäßig bei Pull-Requests den Branch nach dem Mergen löschen", "repo.settings.pulls.default_allow_edits_from_maintainers": "Änderungen von Maintainern standardmäßig erlauben", "repo.settings.releases_desc": "Repository-Releases aktivieren", @@ -1886,6 +2164,7 @@ "repo.settings.admin_indexer_unindexed": "Unindiziert", "repo.settings.reindex_button": "Zur Warteschlange für erneutes Indexieren hinzufügen", "repo.settings.reindex_requested": "Erneutes Indexieren angefordert", + "repo.settings.admin_enable_close_issues_via_commit_in_any_branch": "Einen Issue mit einem Commit auf einem nicht Standard Branch schließen", "repo.settings.danger_zone": "Gefahrenzone", "repo.settings.new_owner_has_same_repo": "Der neue Eigentümer hat bereits ein Repository mit dem gleichen Namen. Bitte wähle einen anderen Namen.", "repo.settings.convert": "In ein normales Repository umwandeln", @@ -1906,7 +2185,9 @@ "repo.settings.transfer_abort_invalid": "Du kannst nur eingeleitete Repository-Übertragung abbrechen.", "repo.settings.transfer_abort_success": "Die Repository-Übertragung zu %s wurde abgebrochen.", "repo.settings.transfer_desc": "Übertrage dieses Repository auf einen anderen Benutzer oder eine Organisation, in der du Admin-Rechte hast.", - "repo.settings.transfer_form_title": "Gib den Repository-Namen zur Bestätigung ein:", + "repo.settings.enter_repo_name_to_confirm": "Gib den Repository Namen als Bestätigung ein:", + "repo.settings.enter_repo_full_name_to_confirm": "Den vollständigen Repository Namen (Eigentümer/Name) als Bestätigung eingeben:", + "repo.settings.transfer_in_progress": "Es gibt derzeit eine laufende Übertragung. Bitte brich diese ab, wenn du dieses Repository an einen anderen Benutzer übertragen möchtest.", "repo.settings.transfer_notices_1": "– Du wirst keinen Zugriff mehr haben, wenn der neue Besitzer ein individueller Benutzer ist.", "repo.settings.transfer_notices_2": "– Du wirst weiterhin Zugriff haben, wenn der neue Besitzer eine Organisation ist und du einer der Besitzer bist.", "repo.settings.transfer_notices_3": "- Wenn das Repository privat ist und an einen einzelnen Benutzer übertragen wird, wird sichergestellt, dass der Benutzer mindestens Leserechte hat (und die Berechtigungen werden gegebenenfalls ändert).", @@ -1921,8 +2202,13 @@ "repo.settings.trust_model.default.desc": "Verwende das Standardvertrauensmodell für diese Installation.", "repo.settings.trust_model.collaborator": "Mitarbeiter", "repo.settings.trust_model.collaborator.long": "Mitarbeiter: Vertraue Signaturen von Mitarbeitern", + "repo.settings.trust_model.collaborator.desc": "Gültige Signaturen von Mitarbeitern dieses Projekts werden als \"vertrauenswürdig\" markiert - egal ob sie mit dem Committer übereinstimmen oder nicht. Andernfalls werden gültige Signaturen als \"nicht vertrauenswürdig\" markiert, unabhängig ob die Signatur mit dem Committer übereinstimmt oder nicht.", + "repo.settings.trust_model.committer": "Committer", + "repo.settings.trust_model.committer.long": "Committer: Vertraue Signaturen, die zu Committern passen. Dies stimmt mit GitHub überein und zwingt signierte Commits von Gitea dazu, Gitea als Committer zu haben.", + "repo.settings.trust_model.committer.desc": "Gültige Signaturen von Mitwirkenden werden als \"vertrauenswürdig\" gekennzeichnet, wenn sie mit ihrem Committer übereinstimmen. Ansonsten werden sie als \"nicht übereinstimmend\" markiert. Das führt dazu, dass Gitea auf signierten Commits, bei denen der echte Committer als Co-authored-by: oder Co-committed-by in der Beschreibung eingetragen wurde, als Committer gilt. Der Gitea Standard-Key muss auf einen User in der Datenbank zeigen.", "repo.settings.trust_model.collaboratorcommitter": "Mitarbeiter+Committer", "repo.settings.trust_model.collaboratorcommitter.long": "Mitarbeiter+Committer: Signaturen der Mitarbeiter vertrauen die mit dem Committer übereinstimmen", + "repo.settings.trust_model.collaboratorcommitter.desc": "Gültige Signaturen von Mitarbeitern dieses Projekts werden als \"vertrauenswürdig\" markiert, wenn sie mit dem Committer übereinstimmen. Andernfalls werden gültige Signaturen als \"nicht vertrauenswürdig\" markiert, wenn die Signatur mit dem Committer übereinstimmt als \"nicht übereinstimmend\". Dies zwingt Gitea als Committer bei signierten Commits mit dem tatsächlichen Committer als Co-Authored-By: und Co-Committed-By: Trailer im Commit. Der Standard-Gitea-Schlüssel muss mit einem Benutzer in der Datenbank übereinstimmen.", "repo.settings.wiki_delete": "Wiki-Daten löschen", "repo.settings.wiki_delete_desc": "Das Löschen von Wiki-Daten kann nicht rückgängig gemacht werden. Bitte sei vorsichtig.", "repo.settings.wiki_delete_notices_1": "– Dies löscht und deaktiviert das Wiki für %s.", @@ -1931,6 +2217,7 @@ "repo.settings.delete": "Dieses Repository löschen", "repo.settings.delete_desc": "Wenn dieses Repository gelöscht wurde, gibt es keinen Weg zurück. Bitte sei vorsichtig.", "repo.settings.delete_notices_1": "– Diese Operation KANN NICHT rückgängig gemacht werden.", + "repo.settings.delete_notices_2": "– Die Operation wird das %s-Repository dauerhaft löschen, inklusive der Dateien, Issues, Kommentare und Zugriffseinstellungen.", "repo.settings.delete_notices_fork_1": "- Forks dieses Repositories werden nach dem Löschen unabhängig.", "repo.settings.deletion_success": "Das Repository wurde gelöscht.", "repo.settings.update_settings_success": "Repository-Einstellungen wurden aktualisiert.", @@ -1949,8 +2236,11 @@ "repo.settings.org_not_allowed_to_be_collaborator": "Organisationen können nicht als Mitarbeiter hinzugefügt werden.", "repo.settings.change_team_access_not_allowed": "Nur der Besitzer der Organisation kann die Zugangsrechte des Teams ändern", "repo.settings.team_not_in_organization": "Das Team ist nicht in der gleichen Organisation wie das Repository", + "repo.settings.teams": "Teams", "repo.settings.add_team": "Team hinzufügen", "repo.settings.add_team_duplicate": "Das Team ist dem Repository schon zugeordnet", + "repo.settings.add_team_success": "Das Team hat nun Zugriff auf das Repository.", + "repo.settings.change_team_permission_tip": "Die Team-Berechtigung ist auf der Team-Einstellungsseite festgelegt und kann nicht für ein Repository geändert werden", "repo.settings.delete_team_tip": "Dieses Team hat Zugriff auf alle Repositories und kann nicht entfernt werden", "repo.settings.remove_team_success": "Der Zugriff des Teams auf das Repository wurde zurückgezogen.", "repo.settings.add_webhook": "Webhook hinzufügen", @@ -1959,7 +2249,8 @@ "repo.settings.webhook_deletion": "Webhook löschen", "repo.settings.webhook_deletion_desc": "Das Entfernen eines Webhooks löscht seine Einstellungen und Zustellungsverlauf. Fortfahren?", "repo.settings.webhook_deletion_success": "Webhook wurde entfernt.", - "repo.settings.webhook.test_delivery_desc_disabled": "Um diesen Webhook mit einem Fake-Event zu testen, aktiviere ihn.", + "repo.settings.webhook.test_delivery": "Test Push Ereignis", + "repo.settings.webhook.test_delivery_desc": "Teste diesen Webhook mit einem Fake-Push-Event.", "repo.settings.webhook.request": "Anfrage", "repo.settings.webhook.response": "Antwort", "repo.settings.webhook.headers": "Kopfzeilen", @@ -1970,13 +2261,16 @@ "repo.settings.webhook.delivery.success": "Ein Event wurde zur Sendungs-Warteschlange hinzugefügt. Es kann ein paar Sekunden dauern, bevor es im Verlauf erscheint.", "repo.settings.githooks_desc": "Git-Hooks werden von Git selbst bereitgestellt. Du kannst die Dateien der unterstützten Hooks in der Liste unten bearbeiten, um eigene Operationen einzubinden.", "repo.settings.githook_edit_desc": "Wenn ein Hook nicht aktiv ist, wird der Standardinhalt benutzt. Lasse den Inhalt leer, um den Hook zu deaktivieren.", - "repo.settings.githook_name": "Hook-Name", - "repo.settings.githook_content": "Hook-Inhalt", "repo.settings.update_githook": "Hook aktualisieren", "repo.settings.add_webhook_desc": "Gitea sendet einen POST-Request mit festgelegtem Content-Type an die Ziel-URL. Mehr Informationen findest du in der Anleitung zu Webhooks (Englisch).", "repo.settings.payload_url": "Ziel-URL", "repo.settings.http_method": "HTTP-Methode", "repo.settings.content_type": "POST-Content-Type", + "repo.settings.webhook.name": "Webhook Name", + "repo.settings.webhook.name_helper": "Optional: Geben Sie dieser Webhook optional einen Namen", + "repo.settings.webhook.name_empty": "Unbenannter Webhook", + "repo.settings.secret": "Geheimnis", + "repo.settings.webhook_secret_desc": "Wenn der Webhook Server Secrets unterstützt, können Sie das Webhook Handbuch befolgen und hier ein Secret eingeben.", "repo.settings.slack_username": "Benutzername", "repo.settings.slack_icon_url": "Icon-URL", "repo.settings.slack_color": "Farbe", @@ -1991,15 +2285,22 @@ "repo.settings.event_create_desc": "Branch oder Tag erstellt.", "repo.settings.event_delete": "Löschen", "repo.settings.event_delete_desc": "Branch oder Tag gelöscht.", + "repo.settings.event_fork": "Fork", "repo.settings.event_fork_desc": "Repository geforkt.", + "repo.settings.event_wiki": "Wiki", "repo.settings.event_wiki_desc": "Wiki-Seite erstellt, umbenannt, bearbeitet oder gelöscht.", "repo.settings.event_statuses": "Status", "repo.settings.event_statuses_desc": "Commit-Status von der API aktualisiert.", "repo.settings.event_release": "Erscheinungsdatum", "repo.settings.event_release_desc": "Release in einem Repository veröffentlicht, aktualisiert oder gelöscht.", + "repo.settings.event_push": "Push", + "repo.settings.event_force_push": "Force Push", "repo.settings.event_push_desc": "Git push in ein Repository.", + "repo.settings.event_repository": "Repository", "repo.settings.event_repository_desc": "Repository erstellt oder gelöscht.", "repo.settings.event_header_issue": "Issue Ereignisse", + "repo.settings.event_issues": "Issues", + "repo.settings.event_issues_desc": "Issue geöffnet, geschlossen, wieder geöffnet, bearbeitet oder gelöscht.", "repo.settings.event_issue_assign": "Issue zugewiesen", "repo.settings.event_issue_assign_desc": "Issue zugewiesen oder Zuweisung entfernt.", "repo.settings.event_issue_label": "Issue mit Label versehen", @@ -2010,6 +2311,7 @@ "repo.settings.event_issue_comment_desc": "Issue-Kommentar angelegt, geändert oder gelöscht.", "repo.settings.event_header_pull_request": "Pull-Request-Ereignisse", "repo.settings.event_pull_request": "Pull-Request", + "repo.settings.event_pull_request_desc": "Pull Request geöffnet, geschlossen, wieder geöffnet, bearbeitet oder gelöscht.", "repo.settings.event_pull_request_assign": "Pull-Request zugewiesen", "repo.settings.event_pull_request_assign_desc": "Pull-Request zugewiesen oder Zuweisung entfernt.", "repo.settings.event_pull_request_label": "Pull-Request mit Label versehen", @@ -2027,11 +2329,16 @@ "repo.settings.event_pull_request_approvals": "Zustimmungen zum Pull-Request", "repo.settings.event_pull_request_merge": "Pull-Request-Merge", "repo.settings.event_header_workflow": "Workflow-Ereignisse", + "repo.settings.event_workflow_run": "Workflow-Ausführung", + "repo.settings.event_workflow_run_desc": "Gitea Actions Workflow Job in Warteschlange, wartend, in Bearbeitung oder abgeschlossen.", "repo.settings.event_workflow_job": "Workflow-Jobs", "repo.settings.event_workflow_job_desc": "Gitea Actions Workflow Job in Warteschlange, wartend, in Bearbeitung oder abgeschlossen.", "repo.settings.event_package": "Paket", "repo.settings.event_package_desc": "Paket wurde in einem Repository erstellt oder gelöscht.", "repo.settings.branch_filter": "Branch-Filter", + "repo.settings.branch_filter_desc_1": "Branch (und Ref Name) erlaubt Liste für Push, Branchenerstellung und Branchenerstellungs-Löschungen, die als Glob-Muster angegeben sind. Wenn leer oder *ist, werden Ereignisse für alle Zweige und Tags gemeldet.", + "repo.settings.branch_filter_desc_2": "Benutzen Sie refs/heads/ oder refs/tags/ Präfix um dem vollen Namen zu entsprechen.", + "repo.settings.branch_filter_desc_doc": "Siehe %[2]s Dokumentation für Syntax.", "repo.settings.authorization_header": "Authorization-Header", "repo.settings.authorization_header_desc": "Wird, falls vorhanden, als Authorization-Header mitgesendet. Beispiele: %s.", "repo.settings.active": "Aktiv", @@ -2042,8 +2349,23 @@ "repo.settings.delete_webhook": "Webhook entfernen", "repo.settings.recent_deliveries": "Letzte Zustellungen", "repo.settings.hook_type": "Hook-Typ", + "repo.settings.slack_token": "Token", + "repo.settings.slack_domain": "Domain", "repo.settings.slack_channel": "Kanal", "repo.settings.add_web_hook_desc": "%s in das Repository integrieren.", + "repo.settings.web_hook_name_gitea": "Gitea", + "repo.settings.web_hook_name_gogs": "Gogs", + "repo.settings.web_hook_name_slack": "Slack", + "repo.settings.web_hook_name_discord": "Discord", + "repo.settings.web_hook_name_dingtalk": "DingTalk", + "repo.settings.web_hook_name_telegram": "Telegram", + "repo.settings.web_hook_name_matrix": "Matrix", + "repo.settings.web_hook_name_msteams": "Microsoft Teams", + "repo.settings.web_hook_name_feishu_or_larksuite": "Feishu / Lark Suite", + "repo.settings.web_hook_name_feishu": "Feishu", + "repo.settings.web_hook_name_larksuite": "Lark Suite", + "repo.settings.web_hook_name_wechatwork": "WeCom (Wechat Work)", + "repo.settings.web_hook_name_packagist": "Packagist", "repo.settings.packagist_username": "Benutzername für Packagist", "repo.settings.packagist_api_token": "API-Token", "repo.settings.packagist_package_url": "Paket-URL", @@ -2061,6 +2383,7 @@ "repo.settings.deploy_key_deletion": "Deploy-Key löschen", "repo.settings.deploy_key_deletion_desc": "Nach dem Löschen wird dieser Deploy-Key keinen Zugriff mehr auf dieses Repository haben. Fortfahren?", "repo.settings.deploy_key_deletion_success": "Der Deploy-Key wurde entfernt.", + "repo.settings.branches": "Branches", "repo.settings.protected_branch": "Branch-Schutz", "repo.settings.protected_branch.save_rule": "Regel speichern", "repo.settings.protected_branch.delete_rule": "Regel löschen", @@ -2094,6 +2417,11 @@ "repo.settings.protect_merge_whitelist_committers_desc": "Erlaube Nutzern oder Teams auf der Genehmigungsliste Pull-Requests in diesen Branch zu mergen.", "repo.settings.protect_merge_whitelist_users": "Nutzer, die mergen dürfen:", "repo.settings.protect_merge_whitelist_teams": "Teams, die mergen dürfen:", + "repo.settings.protect_bypass_allowlist": "Branchschutz umgehen", + "repo.settings.protect_enable_bypass_allowlist": "Erlaube bestimmten Nutzern oder Teams die Push-Beschränkungen zu umgehen", + "repo.settings.protect_enable_bypass_allowlist_desc": "Zugelassene Benutzer oder Teams können selbst dann mergen oder pushen, auch wenn Genehmigungen erforderlich sind, Statusprüfungen oder geschützte Datei-Regeln würden sie andernfalls blockieren.", + "repo.settings.protect_bypass_allowlist_users": "Erlaubte Benutzer zur Umgehung des Schutzes:", + "repo.settings.protect_bypass_allowlist_teams": "Erlaubte Benutzer zur Umgehung des Schutzes:", "repo.settings.protect_check_status_contexts": "Statusprüfungen aktivieren", "repo.settings.protect_status_check_patterns": "Statuscheck-Muster:", "repo.settings.protect_status_check_patterns_desc": "Gib Muster ein, um festzulegen, welche Statusüberprüfungen durchgeführt werden müssen, bevor Branches in einen Branch, der dieser Regel entspricht, gemerged werden können. Jede Zeile gibt ein Muster an. Muster dürfen nicht leer sein.", @@ -2135,15 +2463,18 @@ "repo.settings.block_outdated_branch": "Merge blockieren, wenn der Pull-Request veraltet ist", "repo.settings.block_outdated_branch_desc": "Mergen ist nicht möglich, wenn der Head-Branch hinter dem Basis-Branch ist.", "repo.settings.block_admin_merge_override": "Administratoren müssen die Schutzregeln für Branches befolgen", - "repo.settings.default_branch_desc": "Wähle einen Standardbranch für Pull-Requests und Code-Commits:", + "repo.settings.block_admin_merge_override_desc": "Administratoren müssen die Schutzregeln für Branches befolgen und können diese nicht umgehen.", + "repo.settings.default_branch_desc": "Wähle einen Standardbranch für Pull-Requests und Code-Commits.", + "repo.settings.default_target_branch_desc": "Pull-Requests können verschiedene Standard-Zielzweige verwenden, wenn sie in der Pull Requests Sektion der erweiterten Repository-Einstellungen gesetzt sind.", "repo.settings.merge_style_desc": "Merge-Styles", - "repo.settings.default_merge_style_desc": "Standard-Mergeverhalten für Pull-Requests", + "repo.settings.default_merge_style_desc": "Standard-Mergeverhalten", "repo.settings.choose_branch": "Branch wählen…", "repo.settings.no_protected_branch": "Es gibt keine geschützten Branches.", "repo.settings.edit_protected_branch": "Bearbeiten", "repo.settings.protected_branch_required_rule_name": "Regelname erforderlich", "repo.settings.protected_branch_duplicate_rule_name": "Regelname existiert bereits", "repo.settings.protected_branch_required_approvals_min": "Die Anzahl der erforderlichen Zustimmungen darf nicht negativ sein.", + "repo.settings.tags": "Tags", "repo.settings.tags.protection": "Tag-Schutz", "repo.settings.tags.protection.pattern": "Tag Muster", "repo.settings.tags.protection.allowed": "Erlaubt", @@ -2160,7 +2491,13 @@ "repo.settings.matrix.room_id": "Raum-ID", "repo.settings.matrix.message_type": "Nachrichtentyp", "repo.settings.visibility.private.button": "Auf privat setzen", + "repo.settings.visibility.private.text": "Das Ändern der Sichtbarkeit auf privat wird das Repository nicht nur für erlaubte Mitglieder sichtbar machen, sondern kann auch die Beziehung zwischen ihm und Forks, Beobachtern und Sternen entfernen.", "repo.settings.visibility.private.bullet_title": "Das Ändern der Sichtbarkeit auf privat wird:", + "repo.settings.visibility.private.bullet_one": "Mach das Repo nur für zugelassene Mitglieder sichtbar.", + "repo.settings.visibility.private.bullet_two": "Wenden Sie die Sichtbarkeit auf ihre Forks an und entfernen Sie die Beobachter und Sterne.", + "repo.settings.visibility.private.stats_stars": "Dieses Repository hat %d Sterne(n) die verloren gehen könnten.", + "repo.settings.visibility.private.stats_watchers": "Dieses Repository hat %d Watcher(s) die verloren gehen könnten.", + "repo.settings.visibility.private.stats_forks": "Dieses Repository hat %d Fork(s) die zugeordnet sind.", "repo.settings.visibility.public.button": "Auf öffentlich setzen", "repo.settings.visibility.public.text": "Das Ändern der Sichtbarkeit auf öffentlich macht das Repository für jeden sichtbar.", "repo.settings.visibility.public.bullet_title": "Das Ändern der Sichtbarkeit auf öffentlich wird:", @@ -2179,9 +2516,11 @@ "repo.settings.archive.mirrors_unavailable": "Mirrors sind nicht verfügbar, wenn das Repository archiviert ist.", "repo.settings.unarchive.button": "Archivieren rückgängig machen", "repo.settings.unarchive.header": "Archivieren dieses Repositories rückgängig machen", + "repo.settings.unarchive.text": "Durch das Aufheben der Archivierung kann das Repo wieder Commits und Pushes sowie neue Issues und Pull-Requests empfangen.", "repo.settings.unarchive.success": "Die Archivierung des Repos wurde erfolgreich wieder rückgängig gemacht.", "repo.settings.unarchive.error": "Beim Versuch, die Archivierung des Repos aufzuheben, ist ein Fehler aufgetreten. Weitere Details finden sich im Log.", "repo.settings.update_avatar_success": "Der Repository-Avatar wurde aktualisiert.", + "repo.settings.lfs": "LFS", "repo.settings.lfs_filelist": "LFS-Dateien, die in diesem Repository gespeichert sind", "repo.settings.lfs_no_lfs_files": "In diesem Repository sind keine LFS-Dateien gespeichert", "repo.settings.lfs_findcommits": "Commits finden", @@ -2195,9 +2534,13 @@ "repo.settings.lfs_invalid_lock_directory": "Verzeichnis kann nicht gesperrt werden: %s", "repo.settings.lfs_lock_already_exists": "Sperre existiert bereits: %s", "repo.settings.lfs_lock": "Sperren", + "repo.settings.lfs_lock_path": "Dateipfad zum Sperren…", "repo.settings.lfs_locks_no_locks": "Keine Sperren", "repo.settings.lfs_lock_file_no_exist": "Gesperrte Datei existiert nicht im Standard-Branch", "repo.settings.lfs_force_unlock": "Freigabe erzwingen", + "repo.settings.lfs_pointers.found": "%d Blob-Zeiger gefunden - %d assoziiert, %d nicht assoziiert (%d fehlend im Speicher)", + "repo.settings.lfs_pointers.sha": "Blob SHA", + "repo.settings.lfs_pointers.oid": "OID", "repo.settings.lfs_pointers.inRepo": "Im Repo", "repo.settings.lfs_pointers.exists": "Existiert im Speicher", "repo.settings.lfs_pointers.accessible": "Nutzer hat Zugriff", @@ -2225,6 +2568,7 @@ "repo.diff.whitespace_ignore_at_eol": "Ignoriere EOL-whitespace-Änderungen", "repo.diff.stats_desc": " %d geänderte Dateien mit %d neuen und %d gelöschten Zeilen", "repo.diff.stats_desc_file": "%d Änderungen: %d Ergänzungen und %d Löschungen", + "repo.diff.bin": "BIN", "repo.diff.bin_not_shown": "Binäre Datei nicht angezeigt.", "repo.diff.view_file": "Datei anzeigen", "repo.diff.file_before": "Vorher", @@ -2238,6 +2582,7 @@ "repo.diff.show_more": "Mehr anzeigen", "repo.diff.load": "Diff laden", "repo.diff.generated": "generiert", + "repo.diff.vendored": "Vendor", "repo.diff.comment.add_line_comment": "Einzelnen Kommentar hinzufügen", "repo.diff.comment.placeholder": "Kommentieren...", "repo.diff.comment.add_single_comment": "Einzelnen Kommentar hinzufügen", @@ -2253,6 +2598,9 @@ "repo.diff.review.reject": "Änderung anfragen", "repo.diff.review.self_approve": "Pull-Request-Autoren können ihren eigenen Pull-Request nicht genehmigen", "repo.diff.committed_by": "committet von", + "repo.diff.coauthored_by": "co-authored by", + "repo.commits.avatar_stack_and": "und", + "repo.commits.avatar_stack_people": "%d Personen", "repo.diff.protected": "Geschützt", "repo.diff.image.side_by_side": "Nebeneinander", "repo.diff.image.swipe": "Wischen", @@ -2264,9 +2612,12 @@ "repo.diff.submodule_deleted": "Submodul %[1]s gelöscht von %[2]s", "repo.diff.submodule_updated": "Submodul %[1]s aktualisiert: %[2]s", "repo.releases.desc": "Behalte den Überblick über Versionen und Downloads.", + "repo.release.releases": "Veröffentlichungen", "repo.release.detail": "Release-Details", + "repo.release.tags": "Tags", "repo.release.new_release": "Neues Release", "repo.release.draft": "Entwurf", + "repo.release.prerelease": "Pre-Release", "repo.release.stable": "Stabil", "repo.release.latest": "Aktuell", "repo.release.compare": "Vergleichen", @@ -2302,10 +2653,19 @@ "repo.release.tag_name_invalid": "Der Tag-Name ist ungültig.", "repo.release.tag_name_protected": "Der Tag-Name ist geschützt.", "repo.release.tag_already_exist": "Dieser Tag-Name existiert bereits.", + "repo.release.downloads": "Downloads", + "repo.release.download_count": "Downloads: %s", "repo.release.add_tag_msg": "Titel und Beschreibung des Releases als Tag Nachricht verwenden.", "repo.release.add_tag": "Nur Tag erstellen", "repo.release.releases_for": "Releases für %s", "repo.release.tags_for": "Tags für %s", + "repo.release.notes": "Release Notes", + "repo.release.generate_notes": "Generiere Release Notes", + "repo.release.generate_notes_desc": "Fügen Sie automatisch gemergte Pull-Requests und einen Changelog-Link für diese Version hinzu.", + "repo.release.previous_tag": "Vorheriges Tag", + "repo.release.generate_notes_tag_not_found": "Tag \"%s\" existiert nicht in diesem Repository.", + "repo.release.generate_notes_target_not_found": "Das Release-Ziel \"%s\" kann nicht gefunden werden.", + "repo.release.generate_notes_missing_tag": "Geben Sie einen Tagnamen ein, um Release Notes zu generieren.", "repo.branch.name": "Branchname", "repo.branch.already_exists": "Ein Branch mit dem Namen \"%s\" existiert bereits.", "repo.branch.delete_head": "Löschen", @@ -2343,6 +2703,8 @@ "repo.branch.renamed": "Branch %s wurde in %s umbenannt.", "repo.branch.rename_default_or_protected_branch_error": "Nur Administratoren können Standard- oder geschützte Branches umbenennen.", "repo.branch.rename_protected_branch_failed": "Dieser Branch ist durch globale Schutzregeln geschützt.", + "repo.branch.commits_divergence_from": "Commit-Divergenz: %[1]d hinter und %[2]d vor %[3]s", + "repo.branch.commits_no_divergence": "Der gleiche Branch wie %[1]s", "repo.tag.create_tag": "Tag %s erstellen", "repo.tag.create_tag_operation": "Tag erstellen", "repo.tag.confirm_create_tag": "Tag erstellen", @@ -2352,25 +2714,31 @@ "repo.topic.done": "Fertig", "repo.topic.count_prompt": "Du kannst nicht mehr als 25 Themen auswählen", "repo.topic.format_prompt": "Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) und Punkte ('.') enthalten und bis zu 35 Zeichen lang sein. Nur Kleinbuchstaben sind zulässig.", + "repo.find_file.follow_symlink": "Folge diesem Symlink auf den er verweist", "repo.find_file.go_to_file": "Datei suchen", "repo.find_file.no_matching": "Keine passende Datei gefunden", "repo.error.csv.too_large": "Diese Datei kann nicht gerendert werden, da sie zu groß ist.", "repo.error.csv.unexpected": "Diese Datei kann nicht gerendert werden, da sie ein unerwartetes Zeichen in Zeile %d und Spalte %d enthält.", "repo.error.csv.invalid_field_count": "Diese Datei kann nicht gerendert werden, da sie eine falsche Anzahl an Feldern in Zeile %d hat.", "repo.error.broken_git_hook": "Git-Hooks dieses Repositories scheinen defekt zu sein. Bitte folge der Dokumentation, um dies zu beheben, pushe dann ein paar Commits und den Status zu aktualisieren.", + "graphs.component_loading": "%s wird geladen…", "graphs.component_loading_failed": "%s konnten nicht geladen werden", "graphs.component_loading_info": "Dies kann ein wenig dauern …", "graphs.component_failed_to_load": "Ein unerwarteter Fehler ist aufgetreten.", "graphs.code_frequency.what": "Code-Frequenz", "graphs.contributors.what": "Beiträge", "graphs.recent_commits.what": "Neueste Commits", + "graphs.chart_zoom_hint": "Ziehen: Zoom, Shift+Ziehen: Verschieben, Doppelklick: Zoom zurücksetzen", "org.org_name_holder": "Name der Organisation", "org.org_full_name_holder": "Vollständiger Name der Organisation", "org.org_name_helper": "Organisationsnamen sollten kurz und einprägsam sein.", "org.create_org": "Organisation erstellen", "org.repo_updated": "Aktualisiert", "org.members": "Mitglieder", + "org.teams": "Teams", "org.code": "Quelltext", + "org.repos.empty": "Noch keine Repositories vorhanden.", + "org.repos.empty_description": "Erstellen Sie ein Repository um Code mit der Organisation zu teilen.", "org.lower_members": "Mitglieder", "org.lower_repositories": "Repositories", "org.create_new_team": "Neues Team", @@ -2384,17 +2752,24 @@ "org.team_permission_desc": "Berechtigungen", "org.team_unit_desc": "Zugriff auf Repositorybereiche erlauben", "org.team_unit_disabled": "(Deaktiviert)", + "org.form.name_been_taken": "Organisationsname \"%s\" ist bereits vergeben.", "org.form.name_reserved": "Der Organisationsname \"%s\" ist reserviert.", "org.form.name_pattern_not_allowed": "Das Muster \"%s\" ist in Organisationsnamen nicht erlaubt.", "org.form.create_org_not_allowed": "Du bist nicht berechtigt, eine Organisation zu erstellen.", "org.settings": "Einstellungen", "org.settings.options": "Organisation", "org.settings.full_name": "Vollständiger Name", + "org.settings.email": "Kontakt-E-Mail-Adresse", "org.settings.website": "Webseite", "org.settings.location": "Standort", "org.settings.permission": "Berechtigungen", "org.settings.repoadminchangeteam": "Der Repository-Administrator kann den Zugriff für Teams hinzufügen und zurückziehen", "org.settings.visibility": "Sichtbarkeit", + "org.settings.change_visibility": "Sichtbarkeit ändern", + "org.settings.change_visibility_notices_1": "Wenn die Organisation in privat umgewandelt wird, werden die Repository-Sterne entfernt und können nicht wiederhergestellt werden.", + "org.settings.change_visibility_notices_2": "Nichtmitglieder verlieren den Zugriff auf die Repositories der Organisation, wenn die Sichtbarkeit auf privat geändert wird.", + "org.settings.change_visibility_success": "Die Sichtbarkeit der Organisation %s wurde erfolgreich geändert.", + "org.settings.visibility_desc": "Ändern Sie, wer die Organisation und ihre Repositories sehen kann.", "org.settings.visibility.public": "Öffentlich", "org.settings.visibility.limited": "Begrenzt (nur für authentifizierte Benutzer sichtbar)", "org.settings.visibility.limited_shortname": "Begrenzt", @@ -2402,11 +2777,26 @@ "org.settings.visibility.private_shortname": "Privat", "org.settings.update_settings": "Einstellungen speichern", "org.settings.update_setting_success": "Organisationseinstellungen wurden aktualisiert.", + "org.settings.rename": "Organisation umbenennen", + "org.settings.rename_desc": "Das Ändern des Organisationsnamens wird auch die URL deiner Organisation ändern und den alten Namen freigeben.", + "org.settings.rename_success": "Organisation %[1]s wurde erfolgreich in %[2]s umbenannt.", + "org.settings.rename_no_change": "Organisationsname wurde nicht geändert.", + "org.settings.rename_new_org_name": "Neuer Organisationsname", + "org.settings.rename_failed": "Umbenennen der Organisation aufgrund eines internen Fehlers fehlgeschlagen", + "org.settings.rename_notices_1": "Diese Aktion kann NICHT rückgängig gemacht werden.", + "org.settings.rename_notices_2": "Der alte Name wird weiterleiten, bis er wieder beansprucht wird.", "org.settings.update_avatar_success": "Der Organisationsavatar wurde aktualisiert.", "org.settings.delete": "Organisation löschen", "org.settings.delete_account": "Diese Organisation löschen", "org.settings.delete_prompt": "Die Organisation wird dauerhaft gelöscht. Dies KANN NICHT rückgängig gemacht werden!", + "org.settings.name_confirm": "Namen der Organisation als Bestätigung eingeben:", + "org.settings.delete_notices_1": "Dieser Vorgang kann NICHT rückgängig gemacht werden.", + "org.settings.delete_notices_2": "Dieser Vorgang wird alle Repositories von %s dauerhaft löschen, so wie den beinhalteten Code, Fehler, Kommentare, Wiki und Einstellungen der Mitwirkenden.", + "org.settings.delete_notices_3": "Dieser Vorgang wird alle Pakete von %s dauerhaft löschen.", + "org.settings.delete_notices_4": "Dieser Vorgang wird alle Projekte von %s dauerhaft löschen.", "org.settings.confirm_delete_account": "Löschen bestätigen", + "org.settings.delete_failed": "Das Löschen der Organisation ist aufgrund eines internen Fehlers fehlgeschlagen", + "org.settings.delete_successful": "Organisation %s wurde erfolgreich gelöscht.", "org.settings.hooks_desc": "Webhooks hinzufügen, die für alle Repositories dieser Organisation ausgelöst werden.", "org.settings.labels_desc": "Labels hinzufügen, die für alle Repositories dieser Organisation genutzt werden können.", "org.members.membership_visibility": "Sichtbarkeit der Mitgliedschaft:", @@ -2441,7 +2831,10 @@ "org.teams.no_desc": "Dieses Team hat keine Beschreibung", "org.teams.settings": "Einstellungen", "org.teams.owners_permission_desc": "Besitzer haben vollen Zugriff auf alle Repositories und Admin-Rechte für diese Organisation.", + "org.teams.owners_permission_suggestion": "Sie können neue Teams für Mitglieder erstellen, um eine genauere Zugriffskontrolle zu erhalten.", "org.teams.members": "Teammitglieder", + "org.teams.manage_team_member": "Verwalte Teams und Mitglieder", + "org.teams.manage_team_member_prompt": "Mitglieder werden über Teams verwaltet. Fügen Sie Benutzer zu einem Team hinzu, um sie zu dieser Organisation einzuladen.", "org.teams.update_settings": "Einstellungen aktualisieren", "org.teams.delete_team": "Team löschen", "org.teams.add_team_member": "Teammitglied hinzufügen", @@ -2459,6 +2852,7 @@ "org.teams.remove_all_repos_desc": "Dies entfernt alle Repositories von dem Team.", "org.teams.add_all_repos_title": "Alle Repositories hinzufügen", "org.teams.add_all_repos_desc": "Dadurch werden alle Repositories der Organisation dem Team hinzugefügt.", + "org.teams.add_nonexistent_repo": "Das Repository, das du hinzufügen möchtest, existiert nicht. Bitte erstelle es zuerst.", "org.teams.add_duplicate_users": "Dieser Benutzer ist bereits ein Teammitglied.", "org.teams.repos.none": "Dieses Team hat Zugang zu keinem Repository.", "org.teams.members.none": "Keine Mitglieder in diesem Team.", @@ -2470,6 +2864,14 @@ "org.teams.all_repositories_read_permission_desc": "Dieses Team gewährt Lese-Zugriff auf Repositories: Mitglieder können Repositories ansehen und klonen.", "org.teams.all_repositories_write_permission_desc": "Dieses Team gewährt Schreib-Zugriff auf alle Repositories: Mitglieder können Repositories lesen und auf sie pushen.", "org.teams.all_repositories_admin_permission_desc": "Dieses Team gewährt Administrator-Zugriff auf alle Repositories: Mitglieder können Repositories lesen, auf sie pushen und Mitwirkende zu Repositorys hinzufügen.", + "org.teams.visibility": "Sichtbarkeit", + "org.teams.visibility_private": "Privat", + "org.teams.visibility_private_helper": "Sichtbar nur für Teammitglieder und Organisationsinhaber.", + "org.teams.visibility_limited": "Begrenzt", + "org.teams.visibility_limited_helper": "Sichtbar für alle Mitglieder dieser Organisation.", + "org.teams.visibility_public": "Öffentlich", + "org.teams.visibility_public_helper": "Sichtbar für jeden angemeldeten Benutzer.", + "org.teams.owners_visibility_fixed": "Die Sichtbarkeit des Eigentümerteams kann nicht geändert werden.", "org.teams.invite.title": "Du wurdest eingeladen, dem Team %s in der Organisation %s beizutreten.", "org.teams.invite.by": "Von %s eingeladen", "org.teams.invite.description": "Bitte klicke auf die folgende Schaltfläche, um dem Team beizutreten.", @@ -2481,6 +2883,8 @@ "org.worktime.date_range_end": "Enddatum", "org.worktime.query": "Abfrage", "org.worktime.time": "Zeit", + "org.worktime.empty": "Noch keine Arbeitszeitdaten.", + "org.worktime.empty_description": "Passen Sie den Datumsbereich an, um die erfasste Zeit anzuzeigen.", "org.worktime.by_repositories": "Nach Repositories", "org.worktime.by_milestones": "Nach Meilensteinen", "org.worktime.by_members": "Nach Mitgliedern", @@ -2491,12 +2895,40 @@ "admin.users": "Benutzerkonten", "admin.organizations": "Organisationen", "admin.assets": "Code-Assets", + "admin.repositories": "Repositorien", + "admin.hooks": "Webhooks", "admin.integrations": "Integrationen", "admin.authentication": "Authentifizierungsquellen", + "admin.badges": "Badges", + "admin.badges.badges_manage_panel": "Badge Verwaltung", + "admin.badges.details": "Badge Details", + "admin.badges.new_badge": "Neues Badge erstellen", + "admin.badges.slug": "Slug", + "admin.badges.slug_been_taken": "Der Slug ist bereits vergeben.", + "admin.badges.description": "Beschreibung", + "admin.badges.image_url": "Bild URL", + "admin.badges.new_success": "Das Badge \"%s\" wurde erstellt.", + "admin.badges.update_success": "Badge wurde aktualisiert.", + "admin.badges.deletion_success": "Badge wurde gelöscht.", + "admin.badges.edit_badge": "Bearbeite Badge", + "admin.badges.update_badge": "Aktualisiere Badge", + "admin.badges.delete_badge": "Badge löschen", + "admin.badges.delete_badge_desc": "Sind Sie sicher, dass Sie dieses Badge dauerhaft löschen möchten?", + "admin.badges.users_with_badge": "Benutzer mit Badge: %s", + "admin.badges.not_found": "Badge nicht gefunden.", + "admin.badges.user_already_has": "Benutzer hat bereits dieses Badge.", + "admin.badges.user_add_success": "Badge erfolgreich dem Benutzer zugewiesen.", + "admin.badges.user_remove_success": "Badge erfolgreich vom Benutzer entfernt.", + "admin.badges.manage_users": "Benutzer verwalten", + "admin.badges.add_user": "Neuer Benutzer", + "admin.badges.remove_user": "Benutzer entfernen", + "admin.badges.delete_user_desc": "Sind Sie sicher, dass Sie diesen Benutzer aus dem Badge entfernen möchten?", + "admin.emails": "Benutzer E-Mail-Adressen", "admin.config": "Konfiguration", "admin.config_summary": "Übersicht", "admin.config_settings": "Einstellungen", "admin.notices": "Systemmitteilungen", + "admin.monitor": "Überwachung", "admin.first_page": "Erste", "admin.last_page": "Letzte", "admin.total": "Gesamt: %d", @@ -2517,12 +2949,16 @@ "admin.dashboard.task.finished": "Aufgabe: %[1]s, gestartet von %[2]s, wurde beendet", "admin.dashboard.task.unknown": "Unbekannte Aufgabe: %[1]s", "admin.dashboard.cron.started": "Cron gestartet: %[1]s", + "admin.dashboard.cron.process": "Cron: %[1]s", "admin.dashboard.cron.cancelled": "Cron: %[1]s abgebrochen: %[3]s", "admin.dashboard.cron.error": "Fehler in Cron: %s: %[3]s", "admin.dashboard.cron.finished": "Cron: %[1]s ist beendet", "admin.dashboard.delete_inactive_accounts": "Alle nicht aktivierten Konten löschen", + "admin.dashboard.delete_inactive_accounts.started": "Aufgabe zum Löschen aller nicht aktivierten Konten gestartet", "admin.dashboard.delete_repo_archives": "Lösche alle Repository-Archive (ZIP, TAR.GZ, …)", + "admin.dashboard.delete_repo_archives.started": "Aufgabe zum Löschen aller Projektarchiven gestartet", "admin.dashboard.delete_missing_repos": "Alle Repository-Datensätze mit verloren gegangenen Git-Dateien löschen", + "admin.dashboard.delete_missing_repos.started": "Aufgabe zum Löschen aller Repositories ohne Git-Dateien gestartet", "admin.dashboard.delete_generated_repository_avatars": "Generierte Repository-Avatare löschen", "admin.dashboard.sync_repo_branches": "Fehlende Branches aus den Git-Daten in die Datenbank synchronisieren", "admin.dashboard.sync_repo_tags": "Tags von Git-Daten in die Datenbank synchronisieren", @@ -2530,9 +2966,17 @@ "admin.dashboard.repo_health_check": "Healthchecks für alle Repositories ausführen", "admin.dashboard.check_repo_stats": "Überprüfe alle Repository-Statistiken", "admin.dashboard.archive_cleanup": "Alte Repository-Archive löschen", + "admin.dashboard.deleted_branches_cleanup": "Gelöschte Branches bereinigen", "admin.dashboard.update_migration_poster_id": "Migration Poster-IDs updaten", + "admin.dashboard.git_gc_repos": "Garbage Collection auf alle Repositories ausführen", + "admin.dashboard.resync_all_sshkeys": "Die Datei '.ssh/authorized_keys' mit Gitea SSH-Schlüsseln aktualisieren", + "admin.dashboard.resync_all_sshprincipals": "Die Datei '.ssh/authorized_principals' mit Gitea SSH-Schlüsseln aktualisieren", + "admin.dashboard.resync_all_hooks": "Git-Hooks aller Repositories neu synchronisieren (pre-receive, update, post-receive, proc-receive, ...)", "admin.dashboard.reinit_missing_repos": "Alle Git-Repositories neu einlesen, für die Einträge existieren", "admin.dashboard.sync_external_users": "Externe Benutzerdaten synchronisieren", + "admin.dashboard.cleanup_hook_task_table": "Bereinige hook_task Tabelle", + "admin.dashboard.cleanup_packages": "Veraltete Pakete bereinigen", + "admin.dashboard.cleanup_actions": "Abgelaufene Ressourcen von Actions bereinigen", "admin.dashboard.server_uptime": "Server-Uptime", "admin.dashboard.current_goroutine": "Aktuelle Goroutinen", "admin.dashboard.current_memory_usage": "Aktuelle Speichernutzung", @@ -2563,8 +3007,10 @@ "admin.dashboard.last_gc_pause": "Letzte GC-Pause", "admin.dashboard.gc_times": "Anzahl GC", "admin.dashboard.delete_old_actions": "Alle alten Aktionen aus der Datenbank löschen", + "admin.dashboard.delete_old_actions.started": "Löschen aller alten Aktivitäten in der Datenbank gestartet", "admin.dashboard.update_checker": "Update-Checker", "admin.dashboard.delete_old_system_notices": "Alle alten Systemmeldungen aus der Datenbank löschen", + "admin.dashboard.gc_lfs": "Garbage-Collection für LFS Meta-Objekte", "admin.dashboard.stop_zombie_tasks": "Zombie-Aufgaben stoppen", "admin.dashboard.stop_endless_tasks": "Endlose Aktionen stoppen", "admin.dashboard.cancel_abandoned_jobs": "Aufgegebene Jobs abbrechen", @@ -2581,8 +3027,13 @@ "admin.users.admin": "Administrator", "admin.users.restricted": "Eingeschränkt", "admin.users.reserved": "Reserviert", + "admin.users.bot": "Bot", + "admin.users.remote": "Remote", + "admin.users.2fa": "2FA", + "admin.users.repos": "Repos", "admin.users.created": "Registriert am", "admin.users.last_login": "Letzte Anmeldung", + "admin.users.never_login": "Hat sich noch nie eingeloggt", "admin.users.send_register_notify": "Benutzer-Registrierungsbenachrichtigung senden", "admin.users.new_success": "Der Account \"%s\" wurde erstellt.", "admin.users.edit": "Bearbeiten", @@ -2609,8 +3060,11 @@ "admin.users.still_has_org": "Dieser Nutzer ist Mitglied einer Organisation. Du musst ihn zuerst aus allen Organisationen entfernen.", "admin.users.purge": "Benutzer löschen", "admin.users.purge_help": "Erzwinge das Löschen des Benutzers inklusive aller seiner Repositories, Organisationen, Pakete und Kommentare.", + "admin.users.still_own_packages": "Dieser Benutzer besitzt noch ein oder mehrere Pakete. Lösche diese Pakete zuerst.", "admin.users.deletion_success": "Der Account wurde gelöscht.", "admin.users.reset_2fa": "2FA zurücksetzen", + "admin.users.list_status_filter.menu_text": "Filter", + "admin.users.list_status_filter.reset": "Zurücksetzen", "admin.users.list_status_filter.is_active": "Aktiv", "admin.users.list_status_filter.not_active": "Inaktiv", "admin.users.list_status_filter.is_admin": "Administrator", @@ -2626,7 +3080,10 @@ "admin.emails.primary": "Primär", "admin.emails.activated": "Aktiviert", "admin.emails.filter_sort.email": "E-Mail-Adresse", + "admin.emails.filter_sort.email_reverse": "E-Mail-Adresse (umgekehrt)", "admin.emails.filter_sort.name": "Benutzername", + "admin.emails.filter_sort.name_reverse": "Benutzername (umgekehrt)", + "admin.emails.updated": "E-Mail-Adresse aktualisiert", "admin.emails.not_updated": "Fehler beim Aktualisieren der angeforderten E-Mail-Adresse: %v", "admin.emails.duplicate_active": "Diese E-Mail-Adresse wird bereits von einem Nutzer verwendet.", "admin.emails.change_email_header": "E-Mail-Eigenschaften aktualisieren", @@ -2634,14 +3091,19 @@ "admin.emails.delete": "E-Mail löschen", "admin.emails.delete_desc": "Willst du diese E-Mail-Adresse wirklich löschen?", "admin.emails.deletion_success": "Die E-Mail-Adresse wurde gelöscht.", + "admin.emails.delete_primary_email_error": "Du kannst die primäre E-Mail-Adresse nicht löschen.", "admin.orgs.org_manage_panel": "Organisationsverwaltung", + "admin.orgs.name": "Name", + "admin.orgs.teams": "Teams", "admin.orgs.members": "Mitglieder", "admin.orgs.new_orga": "Neue Organisation", "admin.repos.repo_manage_panel": "Repositoryverwaltung", "admin.repos.unadopted": "Nicht übernommene Repositories", "admin.repos.unadopted.no_more": "Keine weiteren nicht übernommenen Repositories gefunden", "admin.repos.owner": "Besitzer", + "admin.repos.name": "Name", "admin.repos.private": "Privat", + "admin.repos.issues": "Issues", "admin.repos.size": "Größe", "admin.repos.lfs_size": "LFS-Größe", "admin.packages.package_manage_panel": "Paketverwaltung", @@ -2651,7 +3113,10 @@ "admin.packages.cleanup.success": "Abgelaufene Daten erfolgreich bereinigt", "admin.packages.owner": "Besitzer", "admin.packages.creator": "Ersteller", + "admin.packages.name": "Name", + "admin.packages.version": "Version", "admin.packages.type": "Typ", + "admin.packages.repository": "Repository", "admin.packages.size": "Größe", "admin.packages.published": "Veröffentlicht", "admin.defaulthooks": "Standard-Webhooks", @@ -2664,6 +3129,7 @@ "admin.systemhooks.update_webhook": "System-Webhook aktualisieren", "admin.auths.auth_manage_panel": "Authentifikationsquellen verwalten", "admin.auths.new": "Authentifizierungsquelle hinzufügen", + "admin.auths.name": "Name", "admin.auths.type": "Typ", "admin.auths.enabled": "Aktiviert", "admin.auths.syncenabled": "Benutzersynchronisation aktivieren", @@ -2671,6 +3137,9 @@ "admin.auths.auth_type": "Authentifizierungstyp", "admin.auths.auth_name": "Authentifizierungsname", "admin.auths.security_protocol": "Sicherheitsprotokoll", + "admin.auths.domain": "Domain", + "admin.auths.host": "Host", + "admin.auths.port": "Port", "admin.auths.bind_dn": "DN binden", "admin.auths.bind_password": "Passwort binden", "admin.auths.user_base": "Basis für Benutzersuche", @@ -2682,6 +3151,7 @@ "admin.auths.attribute_mail": "E-Mail-Attribut", "admin.auths.attribute_ssh_public_key": "Öffentlicher-SSH-Schlüssel-Attribut", "admin.auths.attribute_avatar": "Avatar-Attribut", + "admin.auths.ssh_keys_are_verified": "SSH-Schlüssel in LDAP werden als verifiziert angesehen", "admin.auths.attributes_in_bind": "Hole Attribute im Bind-Kontext", "admin.auths.allow_deactivate_all": "Erlaube ein leeres Suchergebnis, um alle Benutzer zu deaktivieren", "admin.auths.use_paged_search": "Seitensuche verwenden", @@ -2706,6 +3176,7 @@ "admin.auths.skip_tls_verify": "TLS-Verifikation überspringen", "admin.auths.force_smtps": "SMTPS erzwingen", "admin.auths.force_smtps_helper": "SMTPS wird immer auf Port 465 verwendet. Setze dies, um SMTPS auf anderen Ports zu erzwingen. (Sonst wird STARTTLS auf anderen Ports verwendet, wenn es vom Host unterstützt wird.)", + "admin.auths.helo_hostname": "HELO-Hostname", "admin.auths.helo_hostname_helper": "Mit HELO gesendeter Hostname. Leer lassen, um den aktuellen Hostnamen zu senden.", "admin.auths.disable_helo": "HELO deaktivieren", "admin.auths.pam_service_name": "PAM-Dienstname", @@ -2728,20 +3199,32 @@ "admin.auths.oauth2_required_claim_name_helper": "Setze diesen Namen, damit Nutzer aus dieser Quelle sich nur anmelden dürfen, wenn sie einen Claim mit diesem Namen besitzen", "admin.auths.oauth2_required_claim_value": "Benötigter Claim-Wert", "admin.auths.oauth2_required_claim_value_helper": "Setze diesen Wert, damit Nutzer aus dieser Quelle sich nur anmelden dürfen, wenn sie einen Claim mit diesem Namen und Wert besitzen", + "admin.auths.open_id_connect_external_id_claim": "Externer ID-Anspruchsname (optional)", + "admin.auths.open_id_connect_external_id_claim_helper": "Name der Anspruchsangabe, die als externe Identität des Benutzers verwendet werden soll. Der Standardwert ist „sub“. Für Azure AD / Entra ID sollten Sie hier den Wert „oid“ festlegen, um bei der Migration vom Azure AD V2-Anbieter die Kontinuität zu gewährleisten. Hinweis: Für die Anspruchsangabe „oid“ muss der Geltungsbereich „profile“ im obigen Feld „Geltungsbereiche“ enthalten sein.", "admin.auths.oauth2_group_claim_name": "Claim-Name, der Gruppennamen für diese Quelle angibt. (Optional)", + "admin.auths.oauth2_full_name_claim_name": "Bezeichner für vollständigen Namen. (Optional – wenn gesetzt, wird der vollständige Name des Benutzers stets mit diesem Bezeichner synchronisiert)", + "admin.auths.oauth2_ssh_public_key_claim_name": "Bezeichner für den SSH-Public-Key-Claim", + "admin.auths.oauth2_admin_group": "Gruppenanspruchwert für Administratoren. (Optional: Anspruchsname oben)", + "admin.auths.oauth2_restricted_group": "Gruppenanspruchwert für eingeschränkte Benutzer. (Optional: Anspruchsname oben)", + "admin.auths.oauth2_map_group_to_team": "Abgerufene Gruppen den Organisationsteams zuordnen. (Optional — erfordert oben den Anspruchsnamen)", "admin.auths.oauth2_map_group_to_team_removal": "Benutzer aus synchronisierten Teams entfernen, wenn der Benutzer nicht zur entsprechenden Gruppe gehört.", "admin.auths.enable_auto_register": "Automatische Registrierung aktivieren", "admin.auths.sspi_auto_create_users": "Benutzer automatisch anlegen", + "admin.auths.sspi_auto_create_users_helper": "SSPI Authentifizierungsmethode erlauben, automatisch neue Konten für Benutzer anzulegen, die sich zum ersten Mal anmelden", "admin.auths.sspi_auto_activate_users": "Benutzer automatisch aktivieren", "admin.auths.sspi_auto_activate_users_helper": "Erlaube der SSPI Authentifikationsmethode, automatisch neue Benutzerkonten zu aktivieren", "admin.auths.sspi_strip_domain_names": "Domain vom Nutzernamen entfernen", + "admin.auths.sspi_strip_domain_names_helper": "Wenn aktiviert, werden Domainnamen von Logon-Namen entfernt (z.B. \"DOMAIN\\user\" und \"user@example.org\" werde zu \"user\").", "admin.auths.sspi_separator_replacement": "Trennzeichen als Ersatz für \\, / und @", + "admin.auths.sspi_separator_replacement_helper": "Das Zeichen, das verwendet werden soll, um die Trennzeichen von Logon-Namen (z. die \\ in \"DOMAIN\\user\") und User-Principal Namen (z.B. das @ in \"user@example.org\") zu ersetzen.", "admin.auths.sspi_default_language": "Standardsprache für Benutzer", + "admin.auths.sspi_default_language_helper": "Standardsprache für Benutzer, die automatisch von der SSPI Authentifizierungsmethode erstellt wurden. Leer lassen, wenn die Sprache automatisch erkannt werden soll.", "admin.auths.tips": "Tipps", "admin.auths.tips.oauth2.general": "OAuth2-Authentifizierung", "admin.auths.tips.oauth2.general.tip": "Beim Registrieren einer OAuth2-Anwendung sollte die Callback-URL folgendermaßen lauten:", "admin.auths.tip.oauth2_provider": "OAuth2-Anbieter", "admin.auths.tip.bitbucket": "Registriere einen neuen OAuth-Consumer unter %s und füge die Berechtigung 'Account' - 'Read' hinzu", + "admin.auths.tip.nextcloud": "Registrieren Sie einen neuen \"OAuth consumer\" in Ihrer Instanz, indem Sie im Menü \"Einstellungen -> Sicherheit -> OAuth 2.0 Client\" wählen", "admin.auths.tip.dropbox": "Erstelle eine neue App auf %s", "admin.auths.tip.facebook": "Erstelle eine neue Anwendung auf %s und füge das Produkt „Facebook Login“ hinzu", "admin.auths.tip.github": "Erstelle unter %s eine neue OAuth-Anwendung", @@ -2770,11 +3253,8 @@ "admin.config.server_config": "Serverkonfiguration", "admin.config.app_name": "Seitentitel", "admin.config.app_ver": "Gitea-Version", - "admin.config.app_url": "Gitea-Basis-URL", "admin.config.custom_conf": "Konfigurations-Datei-Pfad", "admin.config.custom_file_root_path": "Benutzerdefinierter Root Pfad", - "admin.config.domain": "Server-Domain", - "admin.config.offline_mode": "Lokaler Modus", "admin.config.disable_router_log": "Router-Log deaktivieren", "admin.config.run_user": "Ausführen als", "admin.config.run_mode": "Laufzeit-Modus", @@ -2789,6 +3269,7 @@ "admin.config.ssh_enabled": "Aktiviert", "admin.config.ssh_start_builtin_server": "Eingebauten Server verwenden", "admin.config.ssh_domain": "SSH-Server-Domain", + "admin.config.ssh_port": "Port", "admin.config.ssh_listen_port": "Listen-Port", "admin.config.ssh_root_path": "Wurzelverzeichnis", "admin.config.ssh_minimum_key_size_check": "Prüfung der Mindestschlüssellänge", @@ -2799,7 +3280,11 @@ "admin.config.lfs_http_auth_expiry": "Ablauf der LFS HTTP Authentifizierung", "admin.config.db_config": "Datenbankkonfiguration", "admin.config.db_type": "Typ", + "admin.config.db_host": "Host", + "admin.config.db_name": "Name", "admin.config.db_user": "Benutzername", + "admin.config.db_schema": "Schema", + "admin.config.db_ssl_mode": "SSL", "admin.config.db_path": "Verzeichnis", "admin.config.service_config": "Service-Konfiguration", "admin.config.register_email_confirm": "E-Mail-Bestätigung benötigt zum Registrieren", @@ -2820,7 +3305,6 @@ "admin.config.default_enable_timetracking": "Zeiterfassung standardmäßig aktivieren", "admin.config.default_allow_only_contributors_to_track_time": "Nur Mitarbeitern erlauben, die Zeiterfassung zu nutzen", "admin.config.no_reply_address": "Versteckte E-Mail-Domain", - "admin.config.default_visibility_organization": "Standard-Sichtbarkeit für neue Organisationen", "admin.config.default_enable_dependencies": "Issue-Abhängigkeiten standardmäßig aktivieren", "admin.config.webhook_config": "Webhook-Konfiguration", "admin.config.queue_length": "Warteschlangenlänge", @@ -2829,6 +3313,7 @@ "admin.config.mailer_config": "Mailer-Konfiguration", "admin.config.mailer_enabled": "Aktiviert", "admin.config.mailer_enable_helo": "HELO aktivieren", + "admin.config.mailer_name": "Name", "admin.config.mailer_protocol": "Protokoll", "admin.config.mailer_smtp_addr": "SMTP-Adresse", "admin.config.mailer_smtp_port": "SMTP-Port", @@ -2836,6 +3321,9 @@ "admin.config.mailer_use_sendmail": "Sendmail benutzen", "admin.config.mailer_sendmail_path": "Sendmail-Pfad", "admin.config.mailer_sendmail_args": "Zusätzliche Argumente für Sendmail", + "admin.config.mailer_sendmail_timeout": "Sendmail Timeout", + "admin.config.mailer_use_dummy": "Dummy", + "admin.config.test_email_placeholder": "E-Mail-Adresse (z.B. test@example.com)", "admin.config.send_test_mail": "Test-E-Mail senden", "admin.config.send_test_mail_submit": "Senden", "admin.config.test_mail_failed": "Das Senden der Test-E-Mail an '%s' ist fehlgeschlagen: %v", @@ -2845,15 +3333,20 @@ "admin.config.cache_config": "Cache-Konfiguration", "admin.config.cache_adapter": "Cache-Adapter", "admin.config.cache_interval": "Cache-Intervall", - "admin.config.cache_conn": "Cache-Anbindung", "admin.config.cache_item_ttl": "Cache Item-TTL", "admin.config.cache_test": "Cache testen", "admin.config.cache_test_failed": "Fehler beim Prüfen des Caches: %v.", "admin.config.cache_test_slow": "Cache-Test erfolgreich, aber die Antwortzeit ist langsam: %s.", "admin.config.cache_test_succeeded": "Cache-Test erfolgreich, Antwort in %s erhalten.", + "admin.config.common.start_time": "Startzeit", + "admin.config.common.end_time": "Endzeit", + "admin.config.common.skip_time_check": "Zeit leer lassen (Feld leeren), um die Zeitüberprüfung zu überspringen", + "admin.config.instance_maintenance": "Instanz-Wartung", + "admin.config.instance_maintenance_mode.admin_web_access_only": "Nur Administrator darf auf die Web-Oberfläche zugreifen", + "admin.config.instance_web_banner.enabled": "Banner anzeigen", + "admin.config.instance_web_banner.message_placeholder": "Banner-Nachricht (unterstützt Markdown)", "admin.config.session_config": "Session-Konfiguration", "admin.config.session_provider": "Session-Provider", - "admin.config.provider_config": "Provider-Einstellungen", "admin.config.cookie_name": "Cookie-Name", "admin.config.gc_interval_time": "GC-Intervall", "admin.config.session_life_time": "Session-Lebensdauer", @@ -2861,9 +3354,10 @@ "admin.config.cookie_life_time": "Cookie-Lebensdauer", "admin.config.picture_config": "Bild-und-Profilbild-Konfiguration", "admin.config.picture_service": "Bilderservice", - "admin.config.disable_gravatar": "Gravatar deaktivieren", + "admin.config.enable_gravatar": "Gravatar aktivieren", "admin.config.enable_federated_avatar": "Föderierte Profilbilder einschalten", "admin.config.open_with_editor_app_help": "Die „Öffnen mit“-Editoren für das Klon-Menü. Falls leer, wird die Standardeinstellung verwendet. Erweitern, um die Standardeinstellung zu sehen.", + "admin.config.git_guide_remote_name": "Name des Remote-Repositorys für die Git-Befehle in der Anleitung", "admin.config.git_config": "Git-Konfiguration", "admin.config.git_disable_diff_highlight": "Diff-Syntaxhervorhebung ausschalten", "admin.config.git_max_diff_lines": "Max. Diff-Zeilen (in einer Datei)", @@ -2872,10 +3366,9 @@ "admin.config.git_gc_args": "GC-Argumente", "admin.config.git_migrate_timeout": "Zeitlimit für Migration", "admin.config.git_mirror_timeout": "Zeitlimit für Mirror-Aktualisierung", - "admin.config.git_clone_timeout": "Zeitlimit für Clone", - "admin.config.git_pull_timeout": "Zeitlimit für Pull", "admin.config.git_gc_timeout": "Zeitlimit für GC", "admin.config.log_config": "Konfiguration des Loggings", + "admin.config.logger_name_fmt": "Logger: %s", "admin.config.disabled_logger": "Deaktiviert", "admin.config.access_log_mode": "Access-Log-Modus", "admin.config.access_log_template": "Zugriffslog-Vorlage", @@ -2883,6 +3376,7 @@ "admin.config.set_setting_failed": "Konfiguration von %s ist fehlgeschlagen", "admin.monitor.stats": "Statistiken", "admin.monitor.cron": "Cron-Aufgaben", + "admin.monitor.name": "Name", "admin.monitor.schedule": "Zeitplan", "admin.monitor.next": "Nächste Ausführung", "admin.monitor.previous": "Letzte Ausführung", @@ -2898,9 +3392,11 @@ "admin.monitor.execute_time": "Ausführungszeit", "admin.monitor.last_execution_result": "Ergebnis", "admin.monitor.process.cancel": "Prozess abbrechen", + "admin.monitor.process.cancel_desc": "Abbrechen eines Prozesses kann Datenverlust verursachen", "admin.monitor.process.children": "Subprozesse", "admin.monitor.queues": "Warteschlangen", "admin.monitor.queue": "Warteschlange: %s", + "admin.monitor.queue.name": "Name", "admin.monitor.queue.type": "Typ", "admin.monitor.queue.exemplar": "Beispieltyp", "admin.monitor.queue.numberworkers": "Anzahl der Worker", @@ -2910,6 +3406,7 @@ "admin.monitor.queue.review_add": "Worker hinzufügen / prüfen", "admin.monitor.queue.settings.title": "Pool-Einstellungen", "admin.monitor.queue.settings.desc": "Pools wachsen dynamisch basierend auf der Blockierung der Arbeitswarteschlange.", + "admin.monitor.queue.settings.maxnumberworkers": "Maximale Anzahl an Workern", "admin.monitor.queue.settings.maxnumberworkers.placeholder": "Derzeit %[1]d", "admin.monitor.queue.settings.maxnumberworkers.error": "Die Anzahl der Worker muss eine Zahl sein", "admin.monitor.queue.settings.submit": "Einstellungen aktualisieren", @@ -2925,6 +3422,7 @@ "admin.notices.delete_selected": "Ausgewählte löschen", "admin.notices.delete_all": "Alle Mitteilungen löschen", "admin.notices.type": "Typ", + "admin.notices.type_1": "Repository", "admin.notices.type_2": "Aufgabe", "admin.notices.desc": "Beschreibung", "admin.notices.op": "Aktion", @@ -2932,6 +3430,10 @@ "admin.self_check.no_problem_found": "Bisher wurde kein Problem festgestellt.", "admin.self_check.startup_warnings": "Warnungen beim Start:", "admin.self_check.database_collation_mismatch": "Erwarte Datenbank-Kollation: %s", + "admin.self_check.database_collation_case_insensitive": "Die Datenbank verwendet die Sortierfolge %s, was eine unbeachtete Sortierfolge ist. Obwohl Gitea damit arbeiten könnte, gibt es einige seltene Fälle, die nicht wie erwartet funktionieren.", + "admin.self_check.database_inconsistent_collation_columns": "Datenbank verwendet Sortierfolge %s, aber diese Spalten verwenden falsche Sortierfolgen. Dies kann zu unerwarteten Problemen führen.", + "admin.self_check.database_fix_mysql": "Für MySQL/MariaDB-Benutzer können Sie den Befehl \"gitea doctor convert\" verwenden, um die Sortierprobleme zu beheben oder Sie können das Problem auch manuell mit \"ALTER ... COLLATE ...\" SQL-Abfragen lösen.", + "admin.self_check.database_fix_mssql": "Für MSSQL-Benutzer konnten Sie das Problem derzeit nur manuell mit \"ALTER ... COLLATE ...\" SQL-Abfragen beheben.", "admin.self_check.location_origin_mismatch": "Aktuelle URL (%[1]s) stimmt nicht mit der URL überein, die Gitea (%[2]s) sieht. Wenn du einen Reverse-Proxy verwendest, stelle bitte sicher, dass die Header \"Host\" und \"X-Forwarded-Proto\" korrekt gesetzt sind.", "action.create_repo": "hat das Repository %s erstellt", "action.rename_repo": "hat das Repository von %[1]s zu %[3]s umbenannt", @@ -3005,6 +3507,8 @@ "gpg.error.no_gpg_keys_found": "Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden", "gpg.error.not_signed_commit": "Kein signierter Commit", "gpg.error.failed_retrieval_gpg_keys": "Fehler beim Abrufen eines Keys des Commiter-Kontos", + "gpg.error.probable_bad_signature": "WARNUNG! Obwohl es einen Schlüssel mit dieser ID in der Datenbank gibt, wird dieser nicht überprüft! Dieser Commit ist SUSPICIOUS.", + "gpg.error.probable_bad_default_signature": "WARNUNG! Obwohl der Standardschlüssel diese ID hat, überprüft er diesen Commit nicht! Dieser Commit ist SUSPICIOUS.", "units.unit": "Einheit", "units.error.no_unit_allowed_repo": "Du hast keine Berechtigung, auf etwas in diesem Repository zuzugreifen.", "units.error.unit_not_allowed": "Du hast keine Berechtigung, um auf diesen Repository-Bereich zuzugreifen.", @@ -3022,10 +3526,13 @@ "packages.filter.container.untagged": "Nicht getaggt", "packages.published_by": "%[1]s von %[3]s veröffentlicht", "packages.published_by_in": "%[1]s von %[3]s in %[5]s veröffentlicht", + "packages.installation": "Installation", "packages.about": "Über dieses Paket", "packages.requirements": "Voraussetzungen", "packages.dependencies": "Abhängigkeiten", "packages.keywords": "Schlüsselwörter", + "packages.details": "Details", + "packages.name": "Paketname", "packages.details.author": "Autor", "packages.details.project_site": "Projektseite", "packages.details.repository_site": "Repository-Seite", @@ -3034,31 +3541,48 @@ "packages.assets": "Dateien", "packages.versions": "Versionen", "packages.versions.view_all": "Alle anzeigen", + "packages.dependency.id": "ID", + "packages.dependency.version": "Version", "packages.search_in_external_registry": "In %s suchen", + "packages.alpine.registry": "Richten Sie diese Registry ein, indem Sie die URL in Ihrer /etc/apk/repositories Datei hinzufügen:", "packages.alpine.registry.key": "Lade den öffentlichen RSA-Key der Registry in den /etc/apk/keys/-Ordner, um die Signatur zu überprüfen:", "packages.alpine.registry.info": "Wähle $branch und $repository aus der Liste unten.", "packages.alpine.install": "Nutze folgenden Befehl, um das Paket zu installieren:", "packages.alpine.repository": "Repository-Informationen", + "packages.alpine.repository.branches": "Branches", + "packages.alpine.repository.repositories": "Repositories", "packages.alpine.repository.architectures": "Architekturen", "packages.arch.registry": "Server mit gebrauchtem Repository und Architektur zu /etc/pacman.conf hinzufügen:", "packages.arch.install": "Paket mit pacman synchronisieren:", "packages.arch.repository": "Repository-Informationen", + "packages.arch.repository.repositories": "Repositories", "packages.arch.repository.architectures": "Architekturen", + "packages.cargo.registry": "Richten Sie diese Registry in der Cargo-Konfigurationsdatei ein (z.B. ~/.cargo/config.toml):", "packages.cargo.install": "Um das Paket mit Cargo zu installieren, führe den folgenden Befehl aus:", + "packages.chef.registry": "Richten Sie diese Registry in Ihrer ~/.chef/config.rb Datei ein:", "packages.chef.install": "Nutze folgenden Befehl, um das Paket zu installieren:", + "packages.composer.registry": "Richten Sie diese Registry in Ihrer ~/.composer/config.json Datei ein:", "packages.composer.install": "Nutze folgenden Befehl, um das Paket mit Composer zu installieren:", "packages.composer.dependencies": "Abhängigkeiten", "packages.composer.dependencies.development": "Entwicklungsabhängigkeiten", + "packages.conan.details.repository": "Repository", + "packages.conan.registry": "Diese Registry über die Kommandozeile einrichten:", "packages.conan.install": "Um das Paket mit Conan zu installieren, führe den folgenden Befehl aus:", + "packages.conda.registry": "Richten Sie diese Registry als Conda Repository in Ihrer .condarc Datei ein:", "packages.conda.install": "Um das Paket mit Conda zu installieren, führe den folgenden Befehl aus:", "packages.container.details.type": "Container-Image Typ", "packages.container.details.platform": "Plattform", "packages.container.pull": "Downloade das Container-Image aus der Kommandozeile:", + "packages.container.images": "Images", + "packages.container.digest": "Digest", "packages.container.multi_arch": "Betriebsystem / Architektur", "packages.container.layers": "Container-Image Ebenen", + "packages.container.labels": "Labels", "packages.container.labels.key": "Schlüssel", "packages.container.labels.value": "Wert", + "packages.cran.registry": "Richten Sie diese Registry in Ihrer Rprofile.site Datei ein:", "packages.cran.install": "Nutze folgenden Befehl, um das Paket zu installieren:", + "packages.debian.registry": "Diese Registry über die Kommandozeile einrichten:", "packages.debian.registry.info": "Wähle $distribution und $component aus der Liste unten.", "packages.debian.install": "Nutze folgenden Befehl, um das Paket zu installieren:", "packages.debian.repository": "Repository-Informationen", @@ -3067,11 +3591,16 @@ "packages.debian.repository.architectures": "Architekturen", "packages.generic.download": "Downloade das Paket aus der Kommandozeile:", "packages.go.install": "Installiere das Paket über die Kommandozeile:", + "packages.helm.registry": "Diese Registry über die Kommandozeile einrichten:", "packages.helm.install": "Nutze folgenden Befehl, um das Paket zu installieren:", + "packages.maven.registry": "Richten Sie diese Registry in Ihrem Projekt pom.xml Datei ein:", + "packages.maven.install": "Um das Paket zu verwenden, fügen Sie folgendes in den Abhängigkeiten -Block in der pom.xml Datei ein:", "packages.maven.install2": "Über die Kommandozeile ausführen:", "packages.maven.download": "Nutze folgendes Kommando, um die Dependency herunterzuladen:", + "packages.nuget.registry": "Diese Registry über die Kommandozeile einrichten:", "packages.nuget.install": "Um das Paket mit NuGet zu installieren, führe den folgenden Befehl aus:", "packages.nuget.dependency.framework": "Zielframework", + "packages.npm.registry": "Diese Registry in Ihrem Projekt .npmrc einrichten:", "packages.npm.install": "Um das Paket mit npm zu installieren, führe den folgenden Befehl aus:", "packages.npm.install2": "oder füge es zur package.json-Datei hinzu:", "packages.npm.dependencies": "Abhängigkeiten", @@ -3079,9 +3608,11 @@ "packages.npm.dependencies.bundle": "Gebündelte Abhängigkeiten", "packages.npm.dependencies.peer": "Peer Abhängigkeiten", "packages.npm.dependencies.optional": "Optionale Abhängigkeiten", + "packages.npm.details.tag": "Tag", "packages.pub.install": "Um das Paket mit Dart zu installieren, führe den folgenden Befehl aus:", "packages.pypi.requires": "Erfordert Python", "packages.pypi.install": "Nutze folgenden Befehl, um das Paket mit pip zu installieren:", + "packages.rpm.registry": "Diese Registry über die Kommandozeile einrichten:", "packages.rpm.distros.redhat": "auf RedHat-basierten Distributionen", "packages.rpm.distros.suse": "auf SUSE-basierten Distributionen", "packages.rpm.install": "Nutze folgenden Befehl, um das Paket zu installieren:", @@ -3094,19 +3625,47 @@ "packages.rubygems.dependencies.development": "Entwicklungsabhängigkeiten", "packages.rubygems.required.ruby": "Benötigt Ruby Version", "packages.rubygems.required.rubygems": "Benötigt RubyGem Version", + "packages.swift.registry": "Diese Registry über die Kommandozeile einrichten:", "packages.swift.install": "Füge das Paket deiner Package.swift Datei hinzu:", "packages.swift.install2": "und führe den folgenden Befehl aus:", + "packages.terraform.install": "Legen Sie Ihren Status fest, um das HTTP-Backend zu verwenden", + "packages.terraform.install2": "und führe den folgenden Befehl aus:", + "packages.terraform.lock_status": "Sperrstatus", + "packages.terraform.locked_by": "Gesperrt von %s", + "packages.terraform.unlocked": "Entsperrt", + "packages.terraform.lock": "Sperren", + "packages.terraform.unlock": "Entsperren", + "packages.terraform.lock.success": "Terraform Status wurde erfolgreich gesperrt.", + "packages.terraform.unlock.success": "Terraform Status wurde erfolgreich entsperrt.", + "packages.terraform.lock.error.already_locked": "Terraform Status ist bereits gesperrt.", + "packages.terraform.delete.locked": "Terraform Status ist gesperrt und kann nicht gelöscht werden.", + "packages.terraform.delete.latest": "Die neueste Version eines Terraform Status kann nicht gelöscht werden.", "packages.vagrant.install": "Um eine Vagrant-Box hinzuzufügen, führe den folgenden Befehl aus:", "packages.settings.link": "Dieses Paket einem Repository zuweisen", + "packages.settings.link.description": "Wenn du ein Paket mit einem Repository verknüpfst, wird es in der Paketliste des Repositories angezeigt.", + "packages.settings.link.notice1": "Nur Repositories unter demselben Eigentümer können verlinkt werden.", + "packages.settings.link.notice2": "Die Verlinkung eines Repositorys ändert die Sichtbarkeit des Pakets nicht.", + "packages.settings.link.notice3": "Wenn das Feld leer gelassen wird, wird der Link entfernt.", + "packages.settings.visibility": "Paketsichtbarkeit", + "packages.settings.visibility.inherit": "Die Paketsichtbarkeit wird vom Eigentümer übernommen und kann hier nicht verändert werden. Um es zu ändern, aktualisieren Sie die Sichtbarkeitseinstellungen des Benutzers oder der Organisation, die dieses Paket besitzt.", + "packages.settings.visibility.button": "Sichtbarkeit des Besitzers ändern", "packages.settings.link.select": "Repository auswählen", "packages.settings.link.button": "Repository-Link aktualisieren", "packages.settings.link.success": "Repository-Link wurde erfolgreich aktualisiert.", "packages.settings.link.error": "Fehler beim Aktualisieren des Repository-Links.", + "packages.settings.link.repo_not_found": "Repository %s nicht gefunden.", + "packages.settings.unlink.error": "Fehler beim Entfernen des Repository-Links.", + "packages.settings.unlink.success": "Repository-Link wurde erfolgreich entfernt.", "packages.settings.delete": "Paket löschen", "packages.settings.delete.description": "Das Löschen eines Pakets ist dauerhaft und kann nicht rückgängig gemacht werden.", "packages.settings.delete.notice": "Du bist dabei, %s (%s) zu löschen. Dieser Vorgang ist unwiderruflich. Bist du sicher?", + "packages.settings.delete.notice.package": "Sie sind dabei, %s und alle seine Versionen zu löschen. Dieser Vorgang ist unwiderruflich, sind Sie sicher?", "packages.settings.delete.success": "Das Paket wurde gelöscht.", + "packages.settings.delete.version.success": "Die Paketversion wurde gelöscht.", "packages.settings.delete.error": "Löschen des Pakets fehlgeschlagen.", + "packages.settings.delete.version": "Version löschen", + "packages.settings.delete.confirm": "Zum Bestätigen Paketnamen eingeben", + "packages.settings.delete.invalid_package_name": "Der eingegebene Paketname ist falsch.", "packages.owner.settings.cargo.title": "Cargo-Registry-Index", "packages.owner.settings.cargo.initialize": "Index initialisieren", "packages.owner.settings.cargo.initialize.description": "Ein spezielles Index-Repository wird benötigt, um die Cargo-Registry zu nutzen. Diese Option wird dieses Repository (neu) erstellen und automatisch konfigurieren.", @@ -3115,6 +3674,7 @@ "packages.owner.settings.cargo.rebuild": "Index neu erstellen", "packages.owner.settings.cargo.rebuild.description": "Neubauen kann hilfreich sein, wenn der Index nicht mit den gespeicherten Cargo-Paketen synchronisiert ist.", "packages.owner.settings.cargo.rebuild.error": "Cargo-Index konnte nicht neu erstellt werden: %v", + "packages.owner.settings.cargo.rebuild.success": "Der Cargo-Index wurde erfolgreich neu erstellt.", "packages.owner.settings.cleanuprules.title": "Bereinigungsregeln verwalten", "packages.owner.settings.cleanuprules.add": "Bereinigungsregel hinzufügen", "packages.owner.settings.cleanuprules.edit": "Bereinigungsregel bearbeiten", @@ -3138,6 +3698,7 @@ "packages.owner.settings.chef.title": "Chef-Registry", "packages.owner.settings.chef.keypair": "Schlüsselpaar generieren", "packages.owner.settings.chef.keypair.description": "Ein Schlüsselpaar ist notwendig, um mit der Chef-Registry zu authentifizieren. Wenn du bereits eins erstellt hast, wird dieses durch eine Neuerstellung verworfen.", + "secrets.secrets": "Geheimnisse", "secrets.description": "Secrets werden an bestimmte Aktionen weitergegeben und können nicht anderweitig ausgelesen werden.", "secrets.none": "Noch keine Secrets vorhanden.", "secrets.creation.description": "Beschreibung", @@ -3153,6 +3714,7 @@ "secrets.deletion.success": "Das Secret wurde entfernt.", "secrets.deletion.failed": "Secret konnte nicht entfernt werden.", "secrets.management": "Secret-Verwaltung", + "actions.actions": "Actions", "actions.unit.desc": "Actions verwalten", "actions.status.unknown": "Unbekannt", "actions.status.waiting": "Wartend", @@ -3160,44 +3722,75 @@ "actions.status.success": "Erfolg", "actions.status.failure": "Fehler", "actions.status.cancelled": "Abgebrochen", + "actions.status.cancelling": "Wird abgebrochen", "actions.status.skipped": "Übersprungen", "actions.status.blocked": "Blockiert", "actions.runners": "Runner", "actions.runners.runner_manage_panel": "Runner-Verwaltung", "actions.runners.new": "Neuen Runner erstellen", "actions.runners.new_notice": "Wie man einen Runner startet", + "actions.runners.status": "Status", + "actions.runners.id": "ID", + "actions.runners.name": "Name", "actions.runners.owner_type": "Typ", + "actions.runners.availability": "Verfügbarkeit", "actions.runners.description": "Beschreibung", + "actions.runners.labels": "Labels", "actions.runners.last_online": "Letzte Online-Zeit", + "actions.runners.runner_title": "Runner", "actions.runners.task_list": "Letzte Aufgaben dieses Runners", "actions.runners.task_list.no_tasks": "Es gibt noch keine Aufgabe.", "actions.runners.task_list.run": "Ausführen", + "actions.runners.task_list.status": "Status", + "actions.runners.task_list.repository": "Repository", + "actions.runners.task_list.commit": "Commit", "actions.runners.task_list.done_at": "Fertig um", "actions.runners.edit_runner": "Runner bearbeiten", "actions.runners.update_runner": "Änderungen anwenden", "actions.runners.update_runner_success": "Runner erfolgreich aktualisiert", "actions.runners.update_runner_failed": "Der Runner konnte nicht aktualisiert werden", + "actions.runners.enable_runner": "Aktiviere diesen Runner", + "actions.runners.enable_runner_success": "Runner erfolgreich aktiviert", + "actions.runners.enable_runner_failed": "Fehler beim Aktivieren des Runners", + "actions.runners.disable_runner": "Diesen Runner deaktivieren", + "actions.runners.disable_runner_success": "Runner erfolgreich deaktiviert", + "actions.runners.disable_runner_failed": "Fehler beim Deaktivieren des Runners", "actions.runners.delete_runner": "Diesen Runner löschen", "actions.runners.delete_runner_success": "Runner erfolgreich gelöscht", "actions.runners.delete_runner_failed": "Der Runner konnte nicht gelöscht werden", "actions.runners.delete_runner_header": "Bestätigen, um diesen Runner zu löschen", + "actions.runners.delete_runner_notice": "Wenn eine Aufgabe auf diesem Runner ausgeführt wird, wird sie beendet und als fehlgeschlagen markiert. Dies könnte den Workflow zerstören.", "actions.runners.none": "Keine Runner verfügbar", "actions.runners.status.unspecified": "Unbekannt", "actions.runners.status.idle": "Leerlauf", "actions.runners.status.active": "Aktiv", + "actions.runners.status.offline": "Offline", + "actions.runners.version": "Version", "actions.runners.reset_registration_token": "Registrierungs-Token zurücksetzen", "actions.runners.reset_registration_token_confirm": "Möchtest du den aktuellen Token invalidieren und einen neuen generieren?", "actions.runners.reset_registration_token_success": "Runner-Registrierungstoken erfolgreich zurückgesetzt", "actions.runs.all_workflows": "Alle Workflows", + "actions.runs.other_workflows": "Andere Workflows", + "actions.runs.other_workflows_tooltip": "Workflows, die in diesem Repository ausgeführt wurden, aber nicht im Standard-Branch vorhanden sind.", + "actions.runs.workflow_run_count_1": "%d Workflow läuft", + "actions.runs.workflow_run_count_n": "%d Workflow laufen", + "actions.runs.commit": "Commit", + "actions.runs.run_details": "Run Details", + "actions.runs.workflow_file": "Workflow-Datei", + "actions.runs.workflow_file_no_permission": "Keine Berechtigung zum Anzeigen der Workflow-Datei", "actions.runs.scheduled": "Geplant", "actions.runs.pushed_by": "gepusht von", "actions.runs.invalid_workflow_helper": "Die Workflow-Konfigurationsdatei ist ungültig. Bitte überprüfe Deine Konfigurationsdatei: %s", "actions.runs.no_matching_online_runner_helper": "Kein passender Runner online mit Label: %s", "actions.runs.no_job_without_needs": "Der Workflow muss mindestens einen Job ohne Abhängigkeiten enthalten.", "actions.runs.no_job": "Der Workflow muss mindestens einen Job enthalten", + "actions.runs.invalid_reusable_workflow_uses": "Ungültiger wiederverwendbarer Workflow \"uses\": %s", "actions.runs.actor": "Initiator", + "actions.runs.status": "Status", "actions.runs.actors_no_select": "Alle Initiatoren", "actions.runs.status_no_select": "Alle Status", + "actions.runs.branch": "Branch", + "actions.runs.branches_no_select": "Alle Branches", "actions.runs.no_results": "Keine passenden Ergebnisse gefunden.", "actions.runs.no_workflows": "Es gibt noch keine Workflows.", "actions.runs.no_workflows.quick_start": "Du weißt nicht, wie du mit Gitea Actions loslegst? Siehe die Schnellstart-Anleitung.", @@ -3206,21 +3799,75 @@ "actions.runs.empty_commit_message": "(leere Commit-Nachricht)", "actions.runs.expire_log_message": "Protokolle wurden geleert, weil sie zu alt waren.", "actions.runs.delete": "Workflow-Ausführung löschen", + "actions.runs.cancel": "Workflow abbrechen", "actions.runs.delete.description": "Bist du sicher, dass du diese Workflow-Ausführung dauerhaft löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", "actions.runs.not_done": "Diese Workflow-Ausführung ist noch nicht abgeschlossen.", "actions.runs.view_workflow_file": "Workflow-Datei anzeigen", + "actions.runs.summary": "Übersicht", + "actions.runs.all_jobs": "Alle Jobs", + "actions.runs.job_summaries": "Job summaries", + "actions.runs.expand_caller_jobs": "Jobs dieses wiederverwendbaren Workflow-Aufrufers anzeigen", + "actions.runs.collapse_caller_jobs": "Jobs dieses wiederverwendbaren Workflow-Aufrufers anzeigen", + "actions.runs.attempt": "Versuch", + "actions.runs.latest": "Aktuell", + "actions.runs.latest_attempt": "Letzter Versuch", + "actions.runs.triggered_via": "Ausgelöst über %s", + "actions.runs.rerun_triggered": "Re-run ausgelöst", + "actions.runs.back_to_pull_request": "Zurück zur Pull Request", + "actions.runs.back_to_workflow": "Zurück zum Workflow", + "actions.runs.total_duration": "Gesamtdauer", + "actions.runs.workflow_dependencies": "Workflowabhängigkeiten", + "actions.runs.graph_jobs_count_1": "%d Job", + "actions.runs.graph_jobs_count_n": "%d Jobs", + "actions.runs.graph_dependencies_count_1": "%d Abhängigkeit", + "actions.runs.graph_dependencies_count_n": "%d Abhängigkeiten", + "actions.runs.graph_success_rate": "%s erfolgreich", + "actions.runs.graph_zoom_in": "Zoomen (Strg/Cmd + Scrollen auf Graph)", + "actions.runs.graph_zoom_max": "Bereits bei 100% Zoom", + "actions.runs.graph_zoom_out": "Verkleinern (Strg/Cmd + Scrollen auf Graph)", + "actions.runs.graph_reset_view": "Ansicht zurücksetzen", "actions.workflow.disable": "Workflow deaktivieren", "actions.workflow.disable_success": "Workflow '%s' erfolgreich deaktiviert.", "actions.workflow.enable": "Workflow aktivieren", "actions.workflow.enable_success": "Workflow '%s' erfolgreich aktiviert.", "actions.workflow.disabled": "Workflow ist deaktiviert.", + "actions.workflow.scope_owner": "Besitzer", + "actions.workflow.scope_global": "Global", + "actions.workflow.required": "Erforderlich", + "actions.workflow.scoped_required_cannot_disable": "Dieser Scoped Workflow ist erforderlich und kann nicht deaktiviert werden.", + "actions.scoped_workflows": "Scoped Workflows", + "actions.scoped_workflows.desc_org": "Repositories als Scoped Workflow Quellen registrieren. Workflow-Dateien unter den Workflow-Verzeichnissen eines Quell-Repositorys laufen in jedem Projektarchiv dieser Organisation, im eigenen Kontext des Projektarchivs.", + "actions.scoped_workflows.desc_user": "Repositories als Scoped Workflow Quellen registrieren. Workflow-Dateien unter den Workflow-Verzeichnissen eines Quell-Repositorys laufen in jedem Projektarchiv dieser Organisation, im eigenen Kontext des Projektarchivs.", + "actions.scoped_workflows.desc_global": "Repositories als Scoped Workflow-Quellen registrieren. Workflow-Dateien unter den Workflow-Verzeichnissen eines Quellcode-Repositorys laufen auf jedem Projektarchiv in dieser Instanz im eigenen Kontext des Projektarchivs. Da Quellen auf Instanzenebene auf den Ereignissen jedes Projektarchivs ausgewertet werden, kann die Registrierung bei großen Instanzen Overhead hinzufügen.", + "actions.scoped_workflows.add_help": "Um Scoped Workflows aus einem Repository zu erstellen, übertrage die Workflow-Dateien unter %s in seinem Standard Branch, dann registrieren Sie das Projektarchiv als Quelle unten.", + "actions.scoped_workflows.security_note": "Der Workflow-Inhalt eines Quell-Repositorys wird in jedem Repository ausgeführt, für das er gilt. Die Skripte der einzelnen Schritte sowie deren Ausgabe werden in den Actions-Logs des jeweiligen Repositorys gespeichert und sind für alle sichtbar, die die Actions des konsumierenden Repositorys einsehen können. Das Registrieren eines privaten Repositorys als Quelle legt daher dessen Workflow-Logik über diese Logs offen. Registriere nur Repositorys, deren Workflow-Inhalte mit allen konsumierenden Repositorys geteilt werden dürfen. Wenn ein scoped Workflow einen wiederverwendbaren Workflow aus einem privaten Repository referenziert, stelle sicher, dass jedes konsumierende Repository darauf Lesezugriff hat – andernfalls schlägt der Workflow dort fehl.", + "actions.scoped_workflows.source.add": "Quell-Repository hinzufügen", + "actions.scoped_workflows.source.add_success": "Quell-Repository hinzugefügt.", + "actions.scoped_workflows.source.remove_success": "Quell-Repository entfernt.", + "actions.scoped_workflows.source.not_found": "Repository nicht gefunden.", + "actions.scoped_workflows.required.update_success": "Erforderliche Workflows aktualisiert.", + "actions.scoped_workflows.required.label": "Workflows als erforderlich markieren (ein erforderlicher Workflow kann nicht durch Repositories deaktiviert werden):", + "actions.scoped_workflows.required.patterns": "Erforderliche Statusüberprüfungsmuster", + "actions.scoped_workflows.required.patterns_aria": "Erforderliche Statusüberprüfungsmuster für %s", + "actions.scoped_workflows.required.patterns_note": "nur erzwungen während der Workflow benötigt wird", + "actions.scoped_workflows.required.patterns_hint": "Markieren Sie den Workflow als erforderlich, um seine Statusüberprüfungsmuster zu konfigurieren.", + "actions.scoped_workflows.required.patterns_help": "Ein Statusüberprüfungsmuster (glob) pro Zeile. Ein Pull-Request kann erst zusammengeführt werden, wenn ein Status mit jedem Muster übereinstimmt. Dies wird für jeden Zielzweig erzwungen, der eine Schutzregel hat, auch für einen mit einer eigenen Statusüberprüfung; ein Zielzweig ohne Schutzregel ist nicht ausgeschaltet.", + "actions.scoped_workflows.required.patterns_empty": "Jeder benötigte Workflow benötigt mindestens ein Statusüberprüfungsmuster.", + "actions.scoped_workflows.required.missing_file": "nicht mehr im Quelltext", + "actions.scoped_workflows.required.expected_contexts": "Erwartete Statusüberprüfung (eine Prüfung, die zu einem Muster passt)", + "actions.scoped_workflows.required.no_status_contexts": "Dieser Workflow postet keine Status Checks, ihn als Anforderung zu markieren würde jede Pull Request blockieren. Deaktiviere die Anforderung.", + "actions.scoped_workflows.no_files": "Im Standard-Branch wurden keine Scoped Workflow-Dateien gefunden.", "actions.workflow.run": "Workflow ausführen", + "actions.workflow.create_status_badge": "Status Badge erstellen", + "actions.workflow.status_badge": "Status Badge", + "actions.workflow.status_badge_url": "Badge-URL", "actions.workflow.not_found": "Workflow '%s' wurde nicht gefunden.", "actions.workflow.run_success": "Workflow '%s' erfolgreich ausgeführt.", "actions.workflow.from_ref": "Nutze Workflow von", "actions.workflow.has_workflow_dispatch": "Dieser Workflow hat einen workflow_dispatch Event-Trigger.", "actions.workflow.has_no_workflow_dispatch": "Der Workflow '%s' hat keinen workflow_dispatch Event-Trigger.", "actions.need_approval_desc": "Um Workflows für den Pull-Request eines Forks auszuführen, ist eine Genehmigung erforderlich.", + "actions.approve_all_success": "Alle Workflow-Abläufe wurden erfolgreich genehmigt.", "actions.variables": "Variablen", "actions.variables.management": "Variablenverwaltung", "actions.variables.creation": "Variable hinzufügen", @@ -3238,15 +3885,43 @@ "actions.variables.update.success": "Die Variable wurde bearbeitet.", "actions.logs.always_auto_scroll": "Autoscroll für Logs immer aktivieren", "actions.logs.always_expand_running": "Laufende Logs immer erweitern", + "actions.general": "Allgemein", + "actions.general.enable_actions": "Actions aktivieren", + "actions.general.collaborative_owners_management": "Kollaboratives Eigentümermanagement", + "actions.general.collaborative_owners_management_help": "Als kollaborativer Eigentümer gilt ein Benutzer oder eine Organisation, dessen/deren privates Repository auf die Aktionen und Workflows dieses Repositorys zugreifen kann.", + "actions.general.add_collaborative_owner": "Kollaborativer Besitzer hinzufügen", + "actions.general.collaborative_owner_not_exist": "Der kollaborative Besitzer existiert nicht.", + "actions.general.remove_collaborative_owner": "Kollaborativer Besitzer entfernen", + "actions.general.remove_collaborative_owner_desc": "Das Entfernen dieses Besitzers verhindert, dass dessen Repositories auf die Aktionen hier zugreifen können. Wollen Sie fortfahren?", "projects.deleted.display_name": "Gelöschtes Projekt", "projects.type-1.display_name": "Individuelles Projekt", "projects.type-2.display_name": "Repository-Projekt", "projects.type-3.display_name": "Organisationsprojekt", "projects.enter_fullscreen": "Vollbild", "projects.exit_fullscreen": "Vollbild verlassen", + "git.filemode.changed_filemode": "%[1]s → %[2]s", "git.filemode.directory": "Verzeichnis", "git.filemode.normal_file": "Normale Datei", "git.filemode.executable_file": "Ausführbare Datei", "git.filemode.symbolic_link": "Softlink", - "git.filemode.submodule": "Submodul" + "git.filemode.submodule": "Submodul", + "org.repos.none": "Keine Repositorys.", + "actions.general.permissions": "Actions Token Berechtigungen", + "actions.general.token_permissions.mode": "Standard Token Berechtigungen", + "actions.general.token_permissions.mode.desc": "Ein Actions Job verwendet die Standardberechtigungen, wenn er seine Berechtigungen in der Workflow-Datei nicht angibt.", + "actions.general.token_permissions.mode.permissive": "Zulässig", + "actions.general.token_permissions.mode.permissive.desc": "Lese- und Schreibberechtigungen für das Job Repository.", + "actions.general.token_permissions.mode.restricted": "Eingeschränkt", + "actions.general.token_permissions.mode.restricted.desc": "Nur Leserechte für Inhaltseinheiten (Code, Veröffentlichungen) des Job Repositories.", + "actions.general.token_permissions.override_owner": "Konfiguration der Besitzerebene überschreiben", + "actions.general.token_permissions.override_owner_desc": "Wenn aktiviert, verwendet dieses Projektarchiv seine eigene Aktionen-Konfiguration, anstatt der Besitzer-Ebene (Benutzer oder Organisation) zu folgen.", + "actions.general.token_permissions.maximum": "Maximale Token Berechtigungen", + "actions.general.token_permissions.maximum.description": "Die effektiven Berechtigungen für Aktionen werden durch die maximal zulässigen Berechtigungen begrenzt.", + "actions.general.token_permissions.fork_pr_note": "Wenn ein Job mit einer Pull Requests von einer Fork gestartet wird, überschreiten seine effektiven Berechtigungen nicht die schreibgeschützten Berechtigungen.", + "actions.general.token_permissions.customize_max_permissions": "Maximale Berechtigungen anpassen", + "actions.general.cross_repo": "Repository übergreifender Zugriff", + "actions.general.cross_repo_desc": "Erlaube den Zugriff auf die ausgewählten Repositories (schreibgeschützt) aller Repositories in diesem Eigentümer mit GITEA_TOKEN beim Ausführen von Actions Jobs.", + "actions.general.cross_repo_selected": "Ausgewählte Repositories", + "actions.general.cross_repo_target_repos": "Ziel Repositorys", + "actions.general.cross_repo_add": "Ziel Repository hinzufügen" } diff --git a/options/locale/locale_el-GR.json b/options/locale/locale_el-GR.json index b248c96d3d..6a0782444a 100644 --- a/options/locale/locale_el-GR.json +++ b/options/locale/locale_el-GR.json @@ -183,7 +183,6 @@ "install.lfs_path": "Ριζική Διαδρομή Git LFS", "install.lfs_path_helper": "Τα αρχεία που παρακολουθούνται από το Git LFS θα αποθηκεύονται σε αυτόν τον φάκελο. Αφήστε κενό για να το απενεργοποιήσετε.", "install.run_user": "Εκτέλεση Σαν Χρήστη", - "install.run_user_helper": "Το όνομα του χρήστη του λειτουργικού συστήματος ο οποίος εκτελεί το Gitea. Επισημαίνεται ότι αυτός ο χρήστης πρέπει να έχει πρόσβαση στο ριζικό φάκελο του αποθετηρίου.", "install.domain": "Domain Διακομιστή", "install.domain_helper": "Όνομα domain διακομιστή ή η διεύθυνση του.", "install.ssh_port": "Θύρα της υπηρεσίας SSH", @@ -204,12 +203,6 @@ "install.register_confirm": "Απαιτείται Επιβεβαίωση της Διεύθυνσης Εmail για Εγγραφή", "install.mail_notify": "Ενεργοποίηση Ειδοποιήσεων με Email", "install.server_service_title": "Ρυθμίσεις Διακομιστή και Υπηρεσιών Τρίτων", - "install.offline_mode": "Ενεργοποίηση Τοπικής Λειτουργίας", - "install.offline_mode_popup": "Απενεργοποιήση των δικτύων διανομής περιεχομένου τρίτων και σερβίρετε όλων των πόρων τοπικά.", - "install.disable_gravatar": "Απενεργοποίηση Gravatar", - "install.disable_gravatar_popup": "Απενεργοποιήση του Gravatar και των εξωτερικών πηγών avatar. Θα χρησιμοποιηθεί ένα προεπιλεγμένο avatar εκτός αν ένας χρήστης ανεβάσει τοπικά ένα avatar.", - "install.federated_avatar_lookup": "Ενεργοποίηση Ομόσπονδων Avatars", - "install.federated_avatar_lookup_popup": "Ενεργοποίηση ομόσπονδης αναζήτησης avatar χρησιμοποιώντας το Libravatar.", "install.disable_registration": "Απενεργοποίηση Αυτοεγγραφής", "install.disable_registration_popup": "Απενεργοποίηση αυτοεγγραφής χρήστη. Μόνο οι διαχειριστές θα μπορούν να δημιουργήσουν νέους λογαριασμούς χρηστών.", "install.allow_only_external_registration_popup": "Να Επιτρέπεται Η Εγγραφή Μόνο Μέσω Εξωτερικών Υπηρεσιών", @@ -229,12 +222,10 @@ "install.admin_email": "Διεύθυνση Email", "install.install_btn_confirm": "Εγκατάσταση Gitea", "install.test_git_failed": "Αδυναμία δοκιμής της εντολής 'git': %v", - "install.sqlite3_not_available": "Αυτή η έκδοση Gitea δεν υποστηρίζει την SQLite3. Παρακαλώ κατεβάστε την επίσημη δυαδική έκδοση από το %s (όχι την έκδοση 'gobuild').", "install.invalid_db_setting": "Οι ρυθμίσεις της βάσης δεδομένων δεν είναι έγκυρες: %v", "install.invalid_db_table": "Ο πίνακας βάσης δεδομένων \"%s\" δεν είναι έγκυρος: %v", "install.invalid_repo_path": "Η αρχική διαδρομή των αποθετηρίων δεν είναι έγκυρη: %v", "install.invalid_app_data_path": "Η διαδρομή δεδομένων εφαρμογής (app data) δεν είναι έγκυρη: %v", - "install.run_user_not_match": "Το όνομα χρήστη 'εκτέλεση ως' δεν είναι το τρέχον όνομα χρήστη: %s -> %s", "install.internal_token_failed": "Αποτυχία δημιουργίας εσωτερικού διακριτικού: %v", "install.secret_key_failed": "Αποτυχία δημιουργίας μυστικού κλειδιού: %v", "install.save_config_failed": "Αποτυχία αποθήκευσης ρυθμίσεων: %v", @@ -687,7 +678,6 @@ "settings.permissions_list": "Δικαιώματα:", "settings.manage_oauth2_applications": "Διαχείριση Εφαρμογών Oauth2", "settings.edit_oauth2_application": "Επεξεργασία Εφαρμογής Oauth2", - "settings.oauth2_applications_desc": "Οι εφαρμογές OAuth2 επιτρέπουν στην εξωτερική εφαρμογή σας την ασφαλή ταυτοποίηση των χρηστών σε αυτό το Gitea.", "settings.remove_oauth2_application": "Αφαίρεση Εφαρμογής Oauth2", "settings.remove_oauth2_application_desc": "Η αφαίρεση μιας εφαρμογής OAuth2 θα ανακαλέσει την πρόσβαση σε όλα τα υπογεγραμμένα διακριτικά πρόσβασης. Συνέχεια;", "settings.remove_oauth2_application_success": "Η εφαρμογή έχει διαγραφεί.", @@ -704,7 +694,6 @@ "settings.oauth2_regenerate_secret_hint": "Χάσατε το μυστικό σας;", "settings.oauth2_client_secret_hint": "Το μυστικό δε θα εμφανιστεί ξανά αν κλείσετε ή ανανεώσετε αυτή τη σελίδα. Παρακαλώ βεβαιωθείτε ότι το έχετε αποθηκεύσει.", "settings.oauth2_application_edit": "Επεξεργασία", - "settings.oauth2_application_create_description": "Οι εφαρμογές OAuth2 δίνει πρόσβαση στην εξωτερική εφαρμογή σας σε λογαριασμούς χρηστών σε αυτή την υπηρεσία.", "settings.oauth2_application_remove_description": "Αφαιρώντας μια εφαρμογή OAuth2 θα αποτραπεί η πρόσβαση αυτής, σε εξουσιοδοτημένους λογαριασμούς χρηστών σε αυτή την υπηρεσία. Συνέχεια;", "settings.oauth2_application_locked": "Το Gitea κάνει προεγγραφή σε μερικές εφαρμογές OAuth2 κατά την εκκίνηση αν είναι ενεργοποιημένες στις ρυθμίσεις. Για την αποφυγή απροσδόκητης συμπεριφοράς, αυτές δεν μπορούν ούτε να επεξεργαστούν ούτε να καταργηθούν. Παρακαλούμε ανατρέξτε στην τεκμηρίωση OAuth2 για περισσότερες πληροφορίες.", "settings.authorized_oauth2_applications": "Εξουσιοδοτημένες Εφαρμογές OAuth2", @@ -769,7 +758,6 @@ "repo.visibility_description": "Μόνο ο ιδιοκτήτης ή τα μέλη του οργανισμού εάν έχουν δικαιώματα, θα είναι σε θέση να το δουν.", "repo.visibility_helper": "Αλλάξτε το αποθετήριο σε ιδιωτικό", "repo.visibility_helper_forced": "Ο διαχειριστής σας αναγκάζει τα νέα αποθετήρια να είναι ιδιωτικά.", - "repo.visibility_fork_helper": "(Αλλάζοντας αυτό θα επηρεάσει όλα τα forks.)", "repo.clone_helper": "Χρειάζεστε βοήθεια για τη κλωνοποίηση; Επισκεφθείτε τη Βοήθεια.", "repo.fork_repo": "Δημιουργία ενός Fork", "repo.fork_from": "Fork Από Το", @@ -838,8 +826,6 @@ "repo.blame.ignore_revs.failed": "Αποτυχία αγνόησης των αναθεωρήσεων στο .git-blame-ignore-revs.", "repo.transfer.accept": "Αποδοχή Μεταφοράς", "repo.transfer.reject": "Απόρριψη Μεταφοράς", - "repo.transfer.no_permission_to_accept": "Δεν έχετε άδεια να αποδεχτείτε αυτή τη μεταφορά.", - "repo.transfer.no_permission_to_reject": "Δεν έχετε άδεια να απορρίψετε αυτή τη μεταφορά.", "repo.desc.private": "Ιδιωτικό", "repo.desc.public": "Δημόσιο", "repo.desc.template": "Πρότυπο", @@ -1032,7 +1018,6 @@ "repo.editor.upload_file_is_locked": "Το αρχείο \"%s\" είναι κλειδωμένο από %s.", "repo.editor.upload_files_to_dir": "Μεταφόρτωση αρχείων στο \"%s\"", "repo.editor.cannot_commit_to_protected_branch": "Δεν είναι δυνατή η υποβολή στον προστατευόμενο κλάδο \"%s\".", - "repo.editor.no_commit_to_branch": "Δεν είναι δυνατή η απευθείας υποβολή στο κλάδο επειδή:", "repo.editor.user_no_push_to_branch": "Ο χρήστης δεν μπορεί να κάνει push στο κλάδο", "repo.editor.require_signed_commit": "Ο κλάδος απαιτεί υπογεγραμμένη υποβολή", "repo.editor.cherry_pick": "Ανθολόγηση (cherry-pic) του %s στο:", @@ -1040,7 +1025,6 @@ "repo.commits.desc": "Δείτε το ιστορικό αλλαγών του πηγαίου κώδικα.", "repo.commits.commits": "Υποβολές", "repo.commits.no_commits": "Δεν υπάρχουν κοινές υποβολές. Τα \"%s\" και \"%s\" έχουν εντελώς διαφορετικές ιστορίες.", - "repo.commits.nothing_to_compare": "Αυτοί οι κλάδοι είναι όμοιοι.", "repo.commits.search.tooltip": "Μπορείτε να προθέτετε τις λέξεις-κλειδιά με \"author:\", \"committer:\", \"after:\", ή \"before:\", π.χ. \"επαναφορά author:Alice before:2019-01-13\".", "repo.commits.search_all": "Όλοι Οι Κλάδοι", "repo.commits.author": "Συγγραφέας", @@ -1109,11 +1093,9 @@ "repo.issues.new": "Νέο Ζήτημα", "repo.issues.new.title_empty": "Ο τίτλος δεν μπορεί να είναι κενός", "repo.issues.new.labels": "Σήματα", - "repo.issues.new.no_label": "Χωρίς Σήμα", "repo.issues.new.clear_labels": "Καθαρισμός σημάτων", "repo.issues.new.projects": "Έργα", "repo.issues.new.clear_projects": "Εκκαθάριση έργων", - "repo.issues.new.no_projects": "Χωρίς έργα", "repo.issues.new.open_projects": "Ανοιχτά Έργα", "repo.issues.new.closed_projects": "Κλειστά Έργα", "repo.issues.new.no_items": "Δεν υπάρχουν αντικείμενα", @@ -1229,7 +1211,6 @@ "repo.issues.close": "Κλείσιμο Ζητήματος", "repo.issues.comment_pull_merged_at": "συγχώνευσε την υποβολή %[1]s στο %[2]s %[3]s", "repo.issues.comment_manually_pull_merged_at": "συγχώνευσε χειροκίνητα την υποβολή %[1]s στο %[2]s %[3]s", - "repo.issues.reopen_issue": "Ανοίξτε ξανά", "repo.issues.create_comment": "Προσθήκη Σχολίου", "repo.issues.closed_at": "αυτό το ζήτημα έκλεισε %[2]s", "repo.issues.reopened_at": "ξανά άνοιξε αυτό το ζήτημα %[2]s", @@ -1413,7 +1394,6 @@ "repo.pulls.allow_edits_from_maintainers": "Επιτρέπεται η επεξεργασία από συντηρητές", "repo.pulls.allow_edits_from_maintainers_desc": "Οι χρήστες με πρόσβαση εγγραφής στον βασικό κλάδο μπορούν επίσης να ωθήσουν και σε αυτό τον κλάδο", "repo.pulls.allow_edits_from_maintainers_err": "Η ενημέρωση απέτυχε", - "repo.pulls.compare_changes_desc": "Επιλέξτε τον κλάδο που θα συγχωνευθεί και τον κλάδο από τον οποίο θα τραβηχτεί.", "repo.pulls.has_viewed_file": "Είδαν", "repo.pulls.has_changed_since_last_review": "Άλλαξε από τη τελευταία κριτική", "repo.pulls.viewed_files_label": "%[1]d / %[2]d αρχεία εμφανίστηκαν", @@ -1430,7 +1410,6 @@ "repo.pulls.showing_specified_commit_range": "Εμφάνιση μόνο των αλλαγών μεταξύ %[1]s..%[2]s", "repo.pulls.review_only_possible_for_full_diff": "Η αξιολόγηση είναι δυνατή μόνο κατά την προβολή της πλήρης διαφοράς", "repo.pulls.filter_changes_by_commit": "Φιλτράρισμα κατά υποβολή", - "repo.pulls.nothing_to_compare": "Αυτοί οι κλάδοι είναι όμοιοι. Δεν υπάρχει ανάγκη να δημιουργήσετε ένα pull request.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "Αυτοί οι κλάδοι είναι ίσοι. Αυτό το PR θα είναι κενό.", "repo.pulls.has_pull_request": "Υπάρχει ήδη pull request μεταξύ αυτών των κλάδων: %[2]s#%[3]d", "repo.pulls.create": "Δημιουργία Pull Request", @@ -1493,7 +1472,6 @@ "repo.pulls.status_checking": "Μερικοί έλεγχοι εκκρεμούν", "repo.pulls.status_checks_success": "Όλοι οι έλεγχοι ήταν επιτυχείς", "repo.pulls.status_checks_warning": "Ορισμένοι έλεγχοι ανέφεραν προειδοποιήσεις", - "repo.pulls.status_checks_failure": "Κάποιοι έλεγχοι απέτυχαν", "repo.pulls.status_checks_error": "Ορισμένοι έλεγχοι ανέφεραν σφάλματα", "repo.pulls.status_checks_requested": "Απαιτείται", "repo.pulls.status_checks_details": "Λεπτομέρειες", @@ -1564,7 +1542,6 @@ "repo.signing.wont_sign.headsigned": "Η συγχώνευση δε θα υπογραφεί καθώς δεν έχει υπογραφή η υποβολή της κεφαλής.", "repo.signing.wont_sign.commitssigned": "Η συγχώνευση δε θα υπογραφεί καθώς όλες οι σχετικές υποβολές δεν έχουν υπογραφεί.", "repo.signing.wont_sign.approved": "Η συγχώνευση δε θα υπογραφεί καθώς το PR δεν έχει εγκριθεί.", - "repo.signing.wont_sign.not_signed_in": "Δεν είστε συνδεδεμένοι.", "repo.ext_wiki": "Πρόσβαση στο Εξωτερικό Wiki", "repo.ext_wiki.desc": "Σύνδεση σε ένα εξωτερικό wiki.", "repo.wiki.welcome": "Καλώς ήρθατε στο Wiki.", @@ -1752,7 +1729,6 @@ "repo.settings.transfer_abort_invalid": "Δεν μπορείτε να ακυρώσετε μια ανύπαρκτη μεταβίβαση αποθετηρίου.", "repo.settings.transfer_abort_success": "Η μεταφορά αποθετηρίου στο %s ακυρώθηκε με επιτυχία.", "repo.settings.transfer_desc": "Μεταβιβάστε αυτό το αποθετήριο σε έναν χρήστη ή σε έναν οργανισμό για τον οποίο έχετε δικαιώματα διαχειριστή.", - "repo.settings.transfer_form_title": "Εισάγετε το όνομα του αποθετηρίου ως επιβεβαίωση:", "repo.settings.transfer_notices_1": "- Θα χάσετε την πρόσβαση στο αποθετήριο αν το μεταβιβάσετε σε έναν μεμονωμένο χρήστη.", "repo.settings.transfer_notices_2": "- Θα διατηρήσετε την πρόσβαση στο αποθετήριο αν το μεταβιβάσετε σε έναν οργανισμό που είστε (συν)ιδιοκτήτης.", "repo.settings.transfer_notices_3": "- Εάν το αποθετήριο είναι ιδιωτικό και μεταβιβάζεται σε μεμονωμένο χρήστη, αυτή η ενέργεια εξασφαλίζει ότι ο χρήστης έχει τουλάχιστον άδεια ανάγνωσης (και αλλάζει τα δικαιώματα εάν είναι απαραίτητο).", @@ -1805,7 +1781,6 @@ "repo.settings.webhook_deletion": "Αφαίρεση Webhook", "repo.settings.webhook_deletion_desc": "Η αφαίρεση ενός webhook διαγράφει τις ρυθμίσεις και το ιστορικό παραδόσεων. Συνέχεια;", "repo.settings.webhook_deletion_success": "Το webhook έχει αφαιρεθεί.", - "repo.settings.webhook.test_delivery_desc_disabled": "Για να δοκιμάσετε αυτό το webhook με μια ψεύτικη κλήση, ενεργοποιήστε το.", "repo.settings.webhook.request": "Αίτημα", "repo.settings.webhook.response": "Απάντηση", "repo.settings.webhook.headers": "Κεφαλίδες", @@ -1816,8 +1791,6 @@ "repo.settings.webhook.delivery.success": "Ένα γεγονός έχει προστεθεί στην ουρά παράδοσης. Μπορεί να χρειαστούν λίγα δευτερόλεπτα μέχρι να εμφανιστεί στο ιστορικό.", "repo.settings.githooks_desc": "Τα Άγκιστρα Git παρέχονται από το ίδιο το Git. Μπορείτε να επεξεργαστείτε τα αρχεία αγκίστρων παρακάτω για να ρυθμίσετε προσαρμοσμένες λειτουργίες.", "repo.settings.githook_edit_desc": "Αν το hook είναι ανενεργό, θα παρουσιαστεί ένα παράδειγμα. Αφήνοντας το περιεχόμενο του hook κενό θα το απενεργοποιήσετε.", - "repo.settings.githook_name": "Όνομα Hook", - "repo.settings.githook_content": "Περιεχόμενο Hook", "repo.settings.update_githook": "Ενημέρωση Hook", "repo.settings.add_webhook_desc": "Ο Gitea θα στείλει αιτήματα POST με συγκεκριμένο τύπο περιεχομένου στο URL προορισμού. Διαβάστε περισσότερα στον οδηγό webhooks.", "repo.settings.payload_url": "URL Στόχου", @@ -1947,7 +1920,6 @@ "repo.settings.block_on_official_review_requests_desc": "Η συγχώνευση δεν θα είναι δυνατή όταν θα έχει αιτήσεις επίσημης αξιολόγησης, ακόμη και αν υπάρχουν αρκετές εγκρίσεις.", "repo.settings.block_outdated_branch": "Φραγή συγχώνευσης αν το pull request είναι ξεπερασμένο", "repo.settings.block_outdated_branch_desc": "Η συγχώνευση δεν θα είναι δυνατή όταν ο κλάδος κεφαλής είναι πίσω από τον βασικό κλάδο.", - "repo.settings.default_branch_desc": "Επιλέξτε έναν προεπιλεγμένο κλάδο αποθετηρίου για pull requests και υποβολές κώδικα:", "repo.settings.merge_style_desc": "Συγχώνευση Στυλ", "repo.settings.choose_branch": "Επιλέξτε έναν κλάδο…", "repo.settings.no_protected_branch": "Δεν υπάρχουν προστατευμένοι κλάδοι.", @@ -2037,8 +2009,6 @@ "repo.diff.too_many_files": "Ορισμένα αρχεία δεν εμφανίστηκαν επειδή έχουν αλλάξει πάρα πολλά αρχεία σε αυτή τη διαφορά", "repo.diff.show_more": "Εμφάνιση Περισσότερων", "repo.diff.load": "Φόρτωση Διαφορών", - "repo.diff.generated": "δημιουργημένο", - "repo.diff.vendored": "εξωτερικό", "repo.diff.comment.add_line_comment": "Προσθήκη σχολίου στη γραμμή", "repo.diff.comment.placeholder": "Αφήστε ένα σχόλιο", "repo.diff.comment.add_single_comment": "Προσθέστε ένα σχόλιο", @@ -2054,6 +2024,7 @@ "repo.diff.review.reject": "Αίτηση αλλαγών", "repo.diff.review.self_approve": "Οι συγγραφείς pull request δεν μπορούν να εγκρίνουν τα δικά τους pull request", "repo.diff.committed_by": "υποβλήθηκε από", + "repo.commits.avatar_stack_and": "και", "repo.diff.protected": "Προστατευμένο", "repo.diff.image.side_by_side": "Δίπλα Δίπλα", "repo.diff.image.swipe": "Σύρσιμο", @@ -2127,7 +2098,6 @@ "repo.branch.restore_success": "Ο κλάδος \"%s\" επαναφέρθηκε.", "repo.branch.restore_failed": "Αποτυχία επαναφοράς του κλάδου \"%s\".", "repo.branch.protected_deletion_failed": "Ο κλάδος \"%s\" προστατεύεται. Δεν μπορεί να διαγραφεί.", - "repo.branch.default_deletion_failed": "Ο κλάδος \"%s\" είναι ο προεπιλεγμένος κλάδος. Δεν μπορεί να διαγραφεί.", "repo.branch.restore": "Επαναφορά του Κλάδου \"%s\"", "repo.branch.download": "Λήψη του Κλάδου \"%s\"", "repo.branch.rename": "Μετονομασία Κλάδου \"%s\"", @@ -2203,9 +2173,7 @@ "org.settings.labels_desc": "Προσθήκη σημάτων που μπορούν να χρησιμοποιηθούν σε ζητήματα για όλα τα αποθετήρια κάτω από αυτό τον οργανισμό.", "org.members.membership_visibility": "Ορατότητα Μέλους:", "org.members.public": "Ορατό", - "org.members.public_helper": "κάνε το κρυφό", "org.members.private": "Κρυφό", - "org.members.private_helper": "κάνε ορατό", "org.members.member_role": "Ρόλος Μέλους:", "org.members.owner": "Ιδιοκτήτης", "org.members.member": "Μέλος", @@ -2235,7 +2203,6 @@ "org.teams.members": "Μέλη Ομάδας", "org.teams.update_settings": "Ενημέρωση Ρυθμίσεων", "org.teams.delete_team": "Διαγραφή Ομάδας", - "org.teams.add_team_member": "Προσθήκη Μέλους Ομάδας", "org.teams.invite_team_member": "Πρόσκληση στο %s", "org.teams.invite_team_member.list": "Εκκρεμείς Προσκλήσεις", "org.teams.delete_team_title": "Διαγραφή Ομάδας", @@ -2260,6 +2227,10 @@ "org.teams.all_repositories_read_permission_desc": "Αυτή η ομάδα χορηγεί πρόσβαση Ανάγνωσης σε όλα τα αποθετήρια: τα μέλη μπορούν να δουν και να κλωνοποιήσουν αποθετήρια.", "org.teams.all_repositories_write_permission_desc": "Αυτή η ομάδα χορηγεί πρόσβαση Εγγραφής σε όλα τα αποθετήρια: τα μέλη μπορούν να διαβάσουν και να κάνουν push σε αποθετήρια.", "org.teams.all_repositories_admin_permission_desc": "Αυτή η ομάδα παρέχει πρόσβαση Διαχείρισης σε όλα τα αποθετήρια: τα μέλη μπορούν να διαβάσουν, να κάνουν push και να προσθέσουν συνεργάτες στα αποθετήρια.", + "org.teams.visibility": "Ορατότητα", + "org.teams.visibility_private": "Ιδιωτικό", + "org.teams.visibility_limited": "Περιορισμένη", + "org.teams.visibility_public": "Δημόσιος", "org.teams.invite.title": "Έχετε προσκληθεί να συμμετάσχετε στην ομάδα %s του οργανισμού %s.", "org.teams.invite.by": "Προσκλήθηκε από %s", "org.teams.invite.description": "Παρακαλώ κάντε κλικ στον παρακάτω σύνδεσμο για συμμετοχή στην ομάδα.", @@ -2271,6 +2242,7 @@ "admin.repositories": "Αποθετήρια", "admin.integrations": "Ενσωματώσεις", "admin.authentication": "Πηγές Ταυτοποίησης", + "admin.badges.description": "Περιγραφή", "admin.config": "Διαμόρφωση", "admin.config_summary": "Περίληψη", "admin.config_settings": "Ρυθμίσεις", @@ -2537,11 +2509,8 @@ "admin.config.server_config": "Ρυθμίσεις Διακομιστή", "admin.config.app_name": "Τίτλος Ιστοτόπου", "admin.config.app_ver": "Έκδοση Gitea", - "admin.config.app_url": "Βασικό URL Του Gitea", "admin.config.custom_conf": "Διαδρομή Αρχείου Ρυθμίσεων", "admin.config.custom_file_root_path": "Προσαρμοσμένη Βασική Διαδρομή Αρχείου", - "admin.config.domain": "Domain Διακομιστή", - "admin.config.offline_mode": "Τοπική Λειτουργία", "admin.config.disable_router_log": "Απενεργοποίηση Καταγραφής Δρομολογητή", "admin.config.run_user": "Εκτέλεση Σαν Χρήστη", "admin.config.run_mode": "Λειτουργία Εκτέλεσης", @@ -2591,7 +2560,6 @@ "admin.config.default_enable_timetracking": "Ενεργοποίηση Καταγραφής Χρόνου σαν Προεπιλογή", "admin.config.default_allow_only_contributors_to_track_time": "Επιτρέπονται Μόνο οι Συμμετέχοντες να Καταγράφουν Χρόνο", "admin.config.no_reply_address": "Κρυφό Email Domain", - "admin.config.default_visibility_organization": "Προεπιλεγμένη ορατότητα για νέους οργανισμούς", "admin.config.default_enable_dependencies": "Ενεργοποίηση Εξαρτήσεων Ζητημάτων από Προεπιλογή", "admin.config.webhook_config": "Ρύθμιση Webhook", "admin.config.queue_length": "Μέγεθος Ουράς", @@ -2619,11 +2587,9 @@ "admin.config.cache_config": "Ρύθμιση Προσωρινής Αποθήκευσης", "admin.config.cache_adapter": "Προσαρμογέας Προσωρινής Αποθήκευσης", "admin.config.cache_interval": "Διάστημα Προσωρινής Αποθήκευσης", - "admin.config.cache_conn": "Σύνδεση Προσωρινής Αποθήκευσης", "admin.config.cache_item_ttl": "TTL Στοιχείων Προσωρινής Αποθήκευσης", "admin.config.session_config": "Ρύθμιση Συνεδρίας", "admin.config.session_provider": "Πάροχος Συνεδρίας", - "admin.config.provider_config": "Ρυθμίσεις Πάροχου", "admin.config.cookie_name": "Όνομα Cookie", "admin.config.gc_interval_time": "Χρόνος Διαστήματος GC", "admin.config.session_life_time": "Χρόνος Ζωής Συνεδρίας", @@ -2631,7 +2597,6 @@ "admin.config.cookie_life_time": "Χρόνος Ζωής Cookie", "admin.config.picture_config": "Ρύθμιση Εικόνας και Avatar", "admin.config.picture_service": "Υπηρεσία Εικόνας", - "admin.config.disable_gravatar": "Απενεργοποίηση Gravatar", "admin.config.enable_federated_avatar": "Ενεργοποίηση Ομόσπονδων Avatars", "admin.config.git_config": "Ρύθμιση Git", "admin.config.git_disable_diff_highlight": "Απενεργοποίηση Επισήμανσης Σύνταξης Diff", @@ -2641,8 +2606,6 @@ "admin.config.git_gc_args": "Παράμετροι GC", "admin.config.git_migrate_timeout": "Χρονικό Όριο Μεταφοράς", "admin.config.git_mirror_timeout": "Χρονικό Όριο Ενημέρωσης Ειδώλου", - "admin.config.git_clone_timeout": "Χρονικό Όριο Κλωνοποίησης", - "admin.config.git_pull_timeout": "Χρονικό Όριο Pull", "admin.config.git_gc_timeout": "Χρονικό Όριο Λειτουργίας GC", "admin.config.log_config": "Ρύθμιση Καταγραφών", "admin.config.logger_name_fmt": "Καταγραφέας: %s", @@ -2863,6 +2826,10 @@ "packages.rubygems.required.rubygems": "Απαιτεί έκδοση RubyGem", "packages.swift.install": "Προσθέστε το πακέτο στο αρχείο Package.swift:", "packages.swift.install2": "και εκτελέστε την ακόλουθη εντολή:", + "packages.terraform.install2": "και εκτελέστε την ακόλουθη εντολή:", + "packages.terraform.lock_status": "Κατάσταση Κλειδώματος", + "packages.terraform.lock": "Κλείδωμα", + "packages.terraform.unlock": "Ξεκλείδωμα", "packages.vagrant.install": "Για προσθήκη ενός κυτίου Vagrant, εκτελέστε την ακόλουθη εντολή:", "packages.settings.link": "Σύνδεση αυτού του πακέτου με ένα αποθετήριο", "packages.settings.link.select": "Επιλογή Αποθετηρίου", @@ -2970,15 +2937,21 @@ "actions.runs.status": "Κατάσταση", "actions.runs.actors_no_select": "Όλοι οι φορείς", "actions.runs.status_no_select": "Όλες οι καταστάσεις", + "actions.runs.branch": "Κλάδος", + "actions.runs.branches_no_select": "Όλοι οι κλάδοι", "actions.runs.no_results": "Δεν βρέθηκαν αποτελέσματα.", "actions.runs.no_workflows": "Δεν υπάρχουν ροές εργασίας ακόμα.", "actions.runs.no_runs": "Η ροή εργασίας δεν έχει τρέξει ακόμα.", "actions.runs.empty_commit_message": "(κενό μήνυμα υποβολής)", + "actions.runs.summary": "Περίληψη", "actions.workflow.disable": "Απενεργοποίηση Ροής Εργασιών", "actions.workflow.disable_success": "Η ροή εργασίας '%s' απενεργοποιήθηκε επιτυχώς.", "actions.workflow.enable": "Ενεργοποίηση Ροής Εργασίας", "actions.workflow.enable_success": "Η ροή εργασίας '%s' ενεργοποιήθηκε επιτυχώς.", "actions.workflow.disabled": "Η ροή εργασιών είναι απενεργοποιημένη.", + "actions.workflow.scope_owner": "Ιδιοκτήτης", + "actions.workflow.scope_global": "Γενικό", + "actions.workflow.required": "Απαιτείται", "actions.need_approval_desc": "Πρέπει να εγκριθεί η εκτέλεση ροών εργασίας για pull request από fork.", "actions.variables": "Μεταβλητές", "actions.variables.management": "Διαχείριση Μεταβλητών", @@ -2998,7 +2971,6 @@ "projects.type-2.display_name": "Έργο Αποθετηρίου", "projects.type-3.display_name": "Έργο Οργανισμού", "git.filemode.directory": "Φάκελος", - "git.filemode.normal_file": "Κανονικό αρχείο", - "git.filemode.executable_file": "Εκτελέσιμο αρχείο", - "git.filemode.submodule": "Υπομονάδα" + "git.filemode.submodule": "Υπομονάδα", + "actions.general.token_permissions.mode.restricted": "Περιορισμένος" } diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index f6e71cccb3..cab8162a62 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -124,6 +124,7 @@ "artifacts": "Artifacts", "expired": "Expired", "artifact_expires_at": "Expires at %s", + "artifact_expired_at": "Expired at %s", "confirm_delete_artifact": "Are you sure you want to delete the artifact '%s'?", "archived": "Archived", "concept_system_global": "Global", @@ -2251,7 +2252,6 @@ "repo.settings.webhook_deletion_success": "The webhook has been removed.", "repo.settings.webhook.test_delivery": "Test Push Event", "repo.settings.webhook.test_delivery_desc": "Test this webhook with a fake push event.", - "repo.settings.webhook.test_delivery_desc_disabled": "To test this webhook with a fake event, activate it.", "repo.settings.webhook.request": "Request", "repo.settings.webhook.response": "Response", "repo.settings.webhook.headers": "Headers", @@ -3306,7 +3306,6 @@ "admin.config.default_enable_timetracking": "Enable Time Tracking by Default", "admin.config.default_allow_only_contributors_to_track_time": "Let Only Contributors Track Time", "admin.config.no_reply_address": "Hidden Email Domain", - "admin.config.default_visibility_organization": "Default visibility for new Organizations", "admin.config.default_enable_dependencies": "Enable Issue Dependencies by Default", "admin.config.webhook_config": "Webhook Configuration", "admin.config.queue_length": "Queue Length", @@ -3779,10 +3778,14 @@ "actions.runs.commit": "Commit", "actions.runs.run_details": "Run Details", "actions.runs.workflow_file": "Workflow file", + "actions.runs.workflow_file_no_permission": "No permission to view the workflow file", "actions.runs.scheduled": "Scheduled", "actions.runs.pushed_by": "pushed by", "actions.runs.invalid_workflow_helper": "Workflow config file is invalid. Please check your config file: %s", "actions.runs.no_matching_online_runner_helper": "No matching online runner with label: %s", + "actions.runs.no_runner_online": "No runner is online to pick up this job.", + "actions.runs.waiting_for_available_runner": "Waiting for a matching runner to become available.", + "actions.runs.waiting_for_dependent_jobs": "Waiting for the following jobs to complete: %s", "actions.runs.no_job_without_needs": "The workflow must contain at least one job without dependencies.", "actions.runs.no_job": "The workflow must contain at least one job", "actions.runs.invalid_reusable_workflow_uses": "Invalid reusable workflow \"uses\": %s", @@ -3832,7 +3835,36 @@ "actions.workflow.enable": "Enable Workflow", "actions.workflow.enable_success": "Workflow '%s' enabled successfully.", "actions.workflow.disabled": "Workflow is disabled.", + "actions.workflow.scope_owner": "Owner", + "actions.workflow.scope_global": "Global", + "actions.workflow.required": "Required", + "actions.workflow.scoped_required_cannot_disable": "This scoped workflow is required and cannot be disabled.", + "actions.scoped_workflows": "Scoped Workflows", + "actions.scoped_workflows.desc_org": "Register repositories as scoped-workflow sources. Workflow files under the scoped workflow directories of a source repository's default branch run on every repository of this organization, in that repository's own context.", + "actions.scoped_workflows.desc_user": "Register repositories as scoped-workflow sources. Workflow files under the scoped workflow directories of a source repository's default branch run on every repository you own, in that repository's own context.", + "actions.scoped_workflows.desc_global": "Register repositories as scoped-workflow sources. Workflow files under the scoped workflow directories of a source repository's default branch run on every repository on this instance, in that repository's own context. Because instance-level sources are evaluated on every repository's events, registering them can add overhead on large instances.", + "actions.scoped_workflows.add_help": "To provide scoped workflows from a repository, commit the workflow files under %s on its default branch, then register the repository as a source below.", + "actions.scoped_workflows.security_note": "A source repository's workflow content is executed in every repository it applies to, and its step scripts and their output are written to that repository's Actions logs and readable by anyone who can view the consuming repository's Actions. Registering a private repository as a source therefore discloses its workflow logic through those logs. Only register repositories whose workflow content may be shared with every consuming repository. If a scoped workflow references a reusable workflow from a private repository, make sure every consuming repository can read it, otherwise the workflow will fail there.", + "actions.scoped_workflows.source.add": "Add source repository", + "actions.scoped_workflows.source.add_success": "Source repository added.", + "actions.scoped_workflows.source.remove_success": "Source repository removed.", + "actions.scoped_workflows.source.not_found": "Repository not found.", + "actions.scoped_workflows.required.update_success": "Required workflows updated.", + "actions.scoped_workflows.required.label": "Mark workflows as required (a required workflow cannot be disabled by repositories):", + "actions.scoped_workflows.required.patterns": "Required status check patterns", + "actions.scoped_workflows.required.patterns_aria": "Required status check patterns for %s", + "actions.scoped_workflows.required.patterns_note": "only enforced while the workflow is required", + "actions.scoped_workflows.required.patterns_hint": "Mark the workflow as required to configure its status check patterns.", + "actions.scoped_workflows.required.patterns_help": "One status check pattern (glob) per line. A consuming pull request can only be merged once a status matching every pattern has passed. This is enforced on any target branch that has a protection rule, even one with its own status checks disabled; a target branch with no protection rule is not gated.", + "actions.scoped_workflows.required.patterns_empty": "Each required workflow needs at least one status check pattern.", + "actions.scoped_workflows.required.missing_file": "file no longer in source", + "actions.scoped_workflows.required.expected_contexts": "Expected status checks (a check that matches a pattern is marked)", + "actions.scoped_workflows.required.no_status_contexts": "This workflow posts no status checks, so marking it required would block every consuming pull request from merging. Uncheck Required.", + "actions.scoped_workflows.no_files": "No scoped workflow files were found on the default branch.", "actions.workflow.run": "Run Workflow", + "actions.workflow.create_status_badge": "Create status badge", + "actions.workflow.status_badge": "Status Badge", + "actions.workflow.status_badge_url": "Badge URL", "actions.workflow.not_found": "Workflow '%s' not found.", "actions.workflow.run_success": "Workflow '%s' run successfully.", "actions.workflow.from_ref": "Use workflow from", diff --git a/options/locale/locale_es-ES.json b/options/locale/locale_es-ES.json index 04727abb5b..088a3e11a2 100644 --- a/options/locale/locale_es-ES.json +++ b/options/locale/locale_es-ES.json @@ -178,7 +178,6 @@ "install.lfs_path": "Ruta raíz de Git LFS", "install.lfs_path_helper": "Los archivos almacenados con Git LFS se almacenarán en este directorio. Déjelo vacío para deshabilitarlo.", "install.run_user": "Ejecutar como usuario", - "install.run_user_helper": "El nombre de usuario del sistema operativo que ejecuta Gitea. Tenga en cuenta que este usuario debe tener acceso a la ruta raíz del repositorio.", "install.domain": "Dominio del Servidor", "install.domain_helper": "Dominio o dirección de host para el servidor.", "install.ssh_port": "Puerto de servidor SSH", @@ -199,12 +198,6 @@ "install.register_confirm": "Requerir confirmación de correo electrónico para registrarse", "install.mail_notify": "Habilitar las notificaciones por correo electrónico", "install.server_service_title": "Configuración del servidor y de servicios de terceros", - "install.offline_mode": "Habilitar autenticación Local", - "install.offline_mode_popup": "Deshabilitar redes de distribución de contenido de terceros y servir todos los recursos localmente.", - "install.disable_gravatar": "Desactivar Gravatar", - "install.disable_gravatar_popup": "Desactivar el Gravatar y fuentes de avatares de terceros. Se utilizará un avatar por defecto a menos que un usuario suba un avatar localmente.", - "install.federated_avatar_lookup": "Habilitar avatares federados", - "install.federated_avatar_lookup_popup": "Habilitar búsqueda de avatares federador para usar el servicio federado de código abierto basado en libravatar.", "install.disable_registration": "Deshabilitar auto-registro", "install.disable_registration_popup": "Deshabilitar auto-registro de usuarios. Sólo los administradores podrán crear nuevas cuentas de usuario.", "install.allow_only_external_registration_popup": "Permitir el registro únicamente a través de servicios externos", @@ -224,12 +217,10 @@ "install.admin_email": "Correo electrónico", "install.install_btn_confirm": "Instalar Gitea", "install.test_git_failed": "Fallo al probar el comando 'git': %v", - "install.sqlite3_not_available": "Esta versión de Gitea no soporta SQLite3. Por favor, descarga la versión binaria oficial de %s (no la versión 'gobuild').", "install.invalid_db_setting": "La configuración de la base de datos no es válida: %v", "install.invalid_db_table": "La tabla \"%s\" de la base de datos no es válida: %v", "install.invalid_repo_path": "La ruta de la raíz del repositorio no es válida: %v", "install.invalid_app_data_path": "La ruta de datos de la aplicación (APP_DATA_PATH) no es válida: %v", - "install.run_user_not_match": "El nombre de usuario 'ejecutar como' no es el nombre actual de usuario: %s -> %s", "install.internal_token_failed": "Fallo al generar el INTERNAL_TOKEN: %v", "install.secret_key_failed": "Fallo al generar el SECRET_KEY: %v", "install.save_config_failed": "Error al guardar la configuración: %v", @@ -678,7 +669,6 @@ "settings.permissions_list": "Permisos:", "settings.manage_oauth2_applications": "Administrar aplicaciones OAuth2", "settings.edit_oauth2_application": "Modificar aplicaciones OAuth2", - "settings.oauth2_applications_desc": "Las aplicaciones OAuth2 permiten a su aplicación de terceros autenticar de forma segura a los usuarios en esta instancia de Gitea.", "settings.remove_oauth2_application": "Eliminar aplicación OAuth2", "settings.remove_oauth2_application_desc": "Eliminar una aplicación OAuth2 revocará el acceso a todos los tokens de acceso firmados. ¿Continuar?", "settings.remove_oauth2_application_success": "La aplicación ha sido eliminada.", @@ -695,7 +685,6 @@ "settings.oauth2_regenerate_secret_hint": "¿Ha perdido su secreto?", "settings.oauth2_client_secret_hint": "El secreto no se mostrará de nuevo después de salir o actualizar esta página. Por favor, asegúrese de haberlo guardado.", "settings.oauth2_application_edit": "Editar", - "settings.oauth2_application_create_description": "Las aplicaciones OAuth2 le dan acceso a su aplicación de terceros a cuentas de usuario en esta instancia.", "settings.oauth2_application_remove_description": "Eliminar una aplicación de OAuth2 evitará que acceda a cuentas de usuario autorizadas en esta instancia. ¿Continuar?", "settings.authorized_oauth2_applications": "Aplicaciones OAuth2 autorizadas", "settings.revoke_key": "Revocar", @@ -754,7 +743,6 @@ "repo.visibility_description": "Sólo el propietario o los miembros de la organización -si tienen derechos- podrán verlo.", "repo.visibility_helper": "Hacer el repositorio privado", "repo.visibility_helper_forced": "El administrador de su sitio obliga a nuevos repositorios a ser privados.", - "repo.visibility_fork_helper": "(Cambiar esto afectará a todos los forks)", "repo.clone_helper": "¿Necesita ayuda para clonar? Visite Ayuda.", "repo.fork_repo": "Hacer fork del repositorio", "repo.fork_from": "Crear un fork desde", @@ -825,8 +813,6 @@ "repo.blame.ignore_revs.failed": "No se pudieron ignorar las revisiones en .git-blame-ignore-revs.", "repo.transfer.accept": "Aceptar transferencia", "repo.transfer.reject": "Rechazar transferencia", - "repo.transfer.no_permission_to_accept": "No tienes permiso para aceptar esta transferencia.", - "repo.transfer.no_permission_to_reject": "No tienes permiso para rechazar esta transferencia.", "repo.desc.private": "Privado", "repo.desc.public": "Público", "repo.desc.template": "Plantilla", @@ -1017,14 +1003,12 @@ "repo.editor.upload_file_is_locked": "El archivo \"%s\" está bloqueado por %s.", "repo.editor.upload_files_to_dir": "Subir archivos a \"%s\"", "repo.editor.cannot_commit_to_protected_branch": "No se puede hacer commit a la rama protegida \"%s\".", - "repo.editor.no_commit_to_branch": "No se puede hacer commit directamente a la rama porque:", "repo.editor.user_no_push_to_branch": "El usuario no puede hacer push a la rama", "repo.editor.require_signed_commit": "Esta rama requiere un commit firmado", "repo.editor.cherry_pick": "Hacer Cherry-pick %s en:", "repo.editor.revert": "Revertir %s en:", "repo.commits.desc": "Ver el historial de cambios de código fuente.", "repo.commits.no_commits": "No hay commits en común. \"%s\" y \"%s\" tienen historias totalmente diferentes.", - "repo.commits.nothing_to_compare": "Estas ramas son iguales.", "repo.commits.search.tooltip": "Puede prefijar palabras clave con \"author:\", \"committer:\", \"after:\", o \"before:\", p. ej., \"revertir author:Alice before:2019-01-13\".", "repo.commits.search_all": "Todas las Ramas", "repo.commits.author": "Autor", @@ -1092,11 +1076,9 @@ "repo.issues.new": "Nueva incidencia", "repo.issues.new.title_empty": "El título no puede estar vacío", "repo.issues.new.labels": "Etiquetas", - "repo.issues.new.no_label": "Sin etiquetas", "repo.issues.new.clear_labels": "Limpiar etiquetas", "repo.issues.new.projects": "Proyectos", "repo.issues.new.clear_projects": "Limpiar proyectos", - "repo.issues.new.no_projects": "Ningún proyecto", "repo.issues.new.open_projects": "Proyectos Abiertos", "repo.issues.new.closed_projects": "Proyectos cerrados", "repo.issues.new.no_items": "No hay elementos", @@ -1212,7 +1194,6 @@ "repo.issues.close": "Cerrar Incidencia", "repo.issues.comment_pull_merged_at": "commit fusionado %[1]s en %[2]s %[3]s", "repo.issues.comment_manually_pull_merged_at": "commit manualmente fusionado %[1]s en %[2]s %[3]s", - "repo.issues.reopen_issue": "Reabrir", "repo.issues.create_comment": "Comentar", "repo.issues.closed_at": "cerró esta incidencia %[2]s", "repo.issues.reopened_at": "reabrió esta incidencia %[2]s", @@ -1395,7 +1376,6 @@ "repo.pulls.allow_edits_from_maintainers": "Permitir ediciones de mantenedores", "repo.pulls.allow_edits_from_maintainers_desc": "Los usuarios con acceso de escritura a la rama base también pueden hacer push a esta rama", "repo.pulls.allow_edits_from_maintainers_err": "Error al Actualizar", - "repo.pulls.compare_changes_desc": "Seleccione la rama en la que se fusiona y la rama a recuperar.", "repo.pulls.has_viewed_file": "Visto", "repo.pulls.has_changed_since_last_review": "Cambiado desde tu última revisión", "repo.pulls.viewed_files_label": "%[1]d / %[2]d archivos vistos", @@ -1412,7 +1392,6 @@ "repo.pulls.showing_specified_commit_range": "Mostrando solo cambios entre %[1]s..%[2]s", "repo.pulls.review_only_possible_for_full_diff": "La revisión solo es posible cuando se ve la diff completa", "repo.pulls.filter_changes_by_commit": "Filtrar por commit", - "repo.pulls.nothing_to_compare": "Estas ramas son iguales. No hay necesidad para crear un pull request.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "Estas ramas son iguales. Este PR estará vacío.", "repo.pulls.has_pull_request": "Ya existe un pull request entre estas ramas: %[2]s#%[3]d", "repo.pulls.create": "Crear Pull Request", @@ -1474,7 +1453,6 @@ "repo.pulls.status_checking": "Algunas comprobaciones están pendientes", "repo.pulls.status_checks_success": "Todas las comprobaciones han sido exitosas", "repo.pulls.status_checks_warning": "Algunas comprobaciones han reportado advertencias", - "repo.pulls.status_checks_failure": "Algunas comprobaciones han fallado", "repo.pulls.status_checks_error": "Algunas comprobaciones reportaron errores", "repo.pulls.status_checks_requested": "Obligatorio", "repo.pulls.status_checks_details": "Detalles", @@ -1539,7 +1517,6 @@ "repo.signing.wont_sign.headsigned": "La fusión no se firmará ya que el commit principal no está firmado.", "repo.signing.wont_sign.commitssigned": "La fusión no se firmará ya que todos los commits asociados no están firmados.", "repo.signing.wont_sign.approved": "La fusión no se firmará ya que el PR no está aprobado.", - "repo.signing.wont_sign.not_signed_in": "No ha iniciado sesión.", "repo.ext_wiki": "Acceso a la wiki externa", "repo.ext_wiki.desc": "Enlace a una wiki externa.", "repo.wiki.welcome": "¡Bienvenidos a la Wiki!", @@ -1725,7 +1702,6 @@ "repo.settings.transfer_abort_invalid": "No puede cancelar una transferencia de repositorio inexistente.", "repo.settings.transfer_abort_success": "La transferencia del repositorio a %s fue cancelada.", "repo.settings.transfer_desc": "Transferir este repositorio a un usuario o una organizacion de la cual disponga de privilegios administrativos.", - "repo.settings.transfer_form_title": "Escriba el nombre del repositorio como confirmación:", "repo.settings.transfer_notices_1": "- Perderá el acceso al repositorio si lo transfiere a un usuario individual.", "repo.settings.transfer_notices_2": "- Mantendrá el acceso al repositorio si lo transfiere a una organización que usted (co-)posee.", "repo.settings.transfer_notices_3": "- Si el repositorio es privado y se transfiere a un usuario individual, esta acción se asegura de que el usuario tenga al menos permisos de lectura (y cambie los permisos si es necesario).", @@ -1777,7 +1753,6 @@ "repo.settings.webhook_deletion": "Eliminar Webhook", "repo.settings.webhook_deletion_desc": "Eliminar un webhook borra sus ajustes e historial de entrega. ¿Continuar?", "repo.settings.webhook_deletion_success": "El webhook ha sido eliminado.", - "repo.settings.webhook.test_delivery_desc_disabled": "Para probar este webhook con un evento falso, actívalo.", "repo.settings.webhook.request": "Petición", "repo.settings.webhook.response": "Respuesta", "repo.settings.webhook.headers": "Encabezado", @@ -1788,8 +1763,6 @@ "repo.settings.webhook.delivery.success": "Se ha añadido un evento a la cola. Puede tardar unos segundos antes de que se muestre en el historial de entrega.", "repo.settings.githooks_desc": "Los Hooks de Git son ejecutados por el propio Git. Puede editar los archivos de hooks a continuación para configurar operaciones personalizadas.", "repo.settings.githook_edit_desc": "Si el hook no está activo, se mostrará contenido de ejemplo. Dejar el contenido vacío deshabilitará este hook.", - "repo.settings.githook_name": "Nombre del Hook", - "repo.settings.githook_content": "Contenido del Hook", "repo.settings.update_githook": "Actualizar Hook", "repo.settings.add_webhook_desc": "Gitea enviará solicitudes POST con un tipo de contenido especificado a la URL de destino. Leer más en la guía webhooks.", "repo.settings.payload_url": "Url destino", @@ -1916,7 +1889,6 @@ "repo.settings.block_on_official_review_requests_desc": "Fusionar no será posible cuando tenga pedidos de revisión oficial, incluso si hay suficientes aprobaciones.", "repo.settings.block_outdated_branch": "Bloquear fusión si la pull request está desactualizada", "repo.settings.block_outdated_branch_desc": "La fusión no será posible cuando la rama principal esté detrás de la rama base.", - "repo.settings.default_branch_desc": "Seleccione una rama de repositorio por defecto para los pull request y los commits:", "repo.settings.merge_style_desc": "Estilos de fusión", "repo.settings.choose_branch": "Elija una rama…", "repo.settings.no_protected_branch": "No hay ramas protegidas.", @@ -2006,8 +1978,6 @@ "repo.diff.too_many_files": "Algunos archivos no se mostraron porque demasiados archivos han cambiado en esta diferencia", "repo.diff.show_more": "Ver más", "repo.diff.load": "Cargar Diff", - "repo.diff.generated": "generado", - "repo.diff.vendored": "vendido", "repo.diff.comment.add_line_comment": "Añadir comentario en línea", "repo.diff.comment.placeholder": "Deja un comentario", "repo.diff.comment.add_single_comment": "Añadir solo comentario", @@ -2023,6 +1993,7 @@ "repo.diff.review.reject": "Solicitud de cambios", "repo.diff.review.self_approve": "Los autores del pull request no pueden aprobarlo", "repo.diff.committed_by": "cometido por", + "repo.commits.avatar_stack_and": "y", "repo.diff.protected": "Protegido", "repo.diff.image.side_by_side": "Lado a lado", "repo.diff.image.swipe": "Deslizar", @@ -2095,7 +2066,6 @@ "repo.branch.restore_success": "La rama \"%s\" ha sido restaurada.", "repo.branch.restore_failed": "Error al restaurar la rama \"%s\".", "repo.branch.protected_deletion_failed": "La rama \"%s\" está protegida. No se puede eliminar.", - "repo.branch.default_deletion_failed": "La rama \"%s\" es la rama por defecto. No se puede eliminar.", "repo.branch.restore": "Restaurar rama \"%s\"", "repo.branch.download": "Descargar rama \"%s\"", "repo.branch.rename": "Renombrar rama \"%s\"", @@ -2171,9 +2141,7 @@ "org.settings.labels_desc": "Añadir etiquetas que pueden ser utilizadas en problemas para todos los repositorios bajo esta organización.", "org.members.membership_visibility": "Visibilidad de Membresía:", "org.members.public": "Público", - "org.members.public_helper": "hacer oculto", "org.members.private": "Oculto", - "org.members.private_helper": "hacer público", "org.members.member_role": "Rol del miembro:", "org.members.owner": "Propietario", "org.members.member": "Miembro", @@ -2203,7 +2171,6 @@ "org.teams.members": "Miembros del equipo", "org.teams.update_settings": "Actualizar configuración", "org.teams.delete_team": "Eliminar equipo", - "org.teams.add_team_member": "Añadir miembro al equipo", "org.teams.invite_team_member": "Invitar a %s", "org.teams.invite_team_member.list": "Invitaciones pendientes", "org.teams.delete_team_title": "Eliminar equipo", @@ -2228,6 +2195,10 @@ "org.teams.all_repositories_read_permission_desc": "Este equipo concede Leer a todos los repositorios: los miembros pueden ver y clonar repositorios.", "org.teams.all_repositories_write_permission_desc": "Este equipo concede Escribir a todos los repositorios: los miembros pueden leer y enviar a los repositorios.", "org.teams.all_repositories_admin_permission_desc": "Este equipo concede a Administrador acceso a todos los repositorios: los miembros pueden leer, enviar y agregar colaboradores a los repositorios.", + "org.teams.visibility": "Visibilidad", + "org.teams.visibility_private": "Privado", + "org.teams.visibility_limited": "Limitado", + "org.teams.visibility_public": "Público", "org.teams.invite.title": "Has sido invitado a unirte al equipo %s en la organización %s.", "org.teams.invite.by": "Invitado por %s", "org.teams.invite.description": "Por favor, haga clic en el botón de abajo para unirse al equipo.", @@ -2239,6 +2210,7 @@ "admin.repositories": "Repositorios", "admin.integrations": "Integraciones", "admin.authentication": "Orígenes de autenticación", + "admin.badges.description": "Descripción", "admin.config": "Configuración", "admin.config_summary": "Resumen", "admin.config_settings": "Configuración", @@ -2501,11 +2473,8 @@ "admin.config.server_config": "Configuración del servidor", "admin.config.app_name": "Título del sitio", "admin.config.app_ver": "Versión de Gitea", - "admin.config.app_url": "URL base de Gitea", "admin.config.custom_conf": "Ruta del fichero de configuración", "admin.config.custom_file_root_path": "Ruta raíz de los archivos personalizada", - "admin.config.domain": "Dominio del Servidor", - "admin.config.offline_mode": "Modo offline", "admin.config.disable_router_log": "Deshabilitar Log del Router", "admin.config.run_user": "Ejecutar como usuario", "admin.config.run_mode": "Modo de ejecución", @@ -2555,7 +2524,6 @@ "admin.config.default_enable_timetracking": "Habilitar seguimiento de tiempo por defecto", "admin.config.default_allow_only_contributors_to_track_time": "Deje que solo los colaboradores hagan un seguimiento del tiempo", "admin.config.no_reply_address": "Dominio de correos electrónicos ocultos", - "admin.config.default_visibility_organization": "Visibilidad por defecto para nuevas organizaciones", "admin.config.default_enable_dependencies": "Habilitar dependencias de incidencias por defecto", "admin.config.webhook_config": "Configuración de Webhooks", "admin.config.queue_length": "Tamaño de Cola de Envío", @@ -2582,11 +2550,9 @@ "admin.config.cache_config": "Configuración de la Caché", "admin.config.cache_adapter": "Adaptador de la Caché", "admin.config.cache_interval": "Intervalo de la Caché", - "admin.config.cache_conn": "Conexión de la Caché", "admin.config.cache_item_ttl": "Período de vida para elementos de caché", "admin.config.session_config": "Configuración de la Sesión", "admin.config.session_provider": "Proveedor de la Sesión", - "admin.config.provider_config": "Configuración del Proveedor", "admin.config.cookie_name": "Nombre de la Cookie", "admin.config.gc_interval_time": "Intervalo de tiempo del GC", "admin.config.session_life_time": "Tiempo de Vida de la Sesión", @@ -2594,7 +2560,6 @@ "admin.config.cookie_life_time": "Tiempo de Vida de la Cookie", "admin.config.picture_config": "Configuración de imagen y avatar", "admin.config.picture_service": "Servicio de Imágen", - "admin.config.disable_gravatar": "Desactivar Gravatar", "admin.config.enable_federated_avatar": "Habilitar Avatares Federados", "admin.config.git_config": "Configuración de Git", "admin.config.git_disable_diff_highlight": "Desactivar resaltado de sintaxis del Diff", @@ -2604,8 +2569,6 @@ "admin.config.git_gc_args": "Argumentos de GC", "admin.config.git_migrate_timeout": "Tiempo de espera de migración", "admin.config.git_mirror_timeout": "Tiempo de espera de actualización de réplicas", - "admin.config.git_clone_timeout": "Tiempo de espera de operación de clones", - "admin.config.git_pull_timeout": "Tiempo de espera de operación de pull", "admin.config.git_gc_timeout": "Tiempo de espera de operación de GC", "admin.config.log_config": "Configuración del Log", "admin.config.logger_name_fmt": "Registro: %s", @@ -2827,6 +2790,10 @@ "packages.rubygems.required.rubygems": "Requiere la versión de RubyGem", "packages.swift.install": "Añade el paquete en tu archivo Package.swift:", "packages.swift.install2": "y ejecuta el siguiente comando:", + "packages.terraform.install2": "y ejecuta el siguiente comando:", + "packages.terraform.lock_status": "Estado de bloqueo", + "packages.terraform.lock": "Bloquear", + "packages.terraform.unlock": "Desbloquear", "packages.vagrant.install": "Para añadir un paquete Vagrant, ejecuta el siguiente comando:", "packages.settings.link": "Vincular este paquete a un repositorio", "packages.settings.link.select": "Seleccionar repositorio", @@ -2931,13 +2898,18 @@ "actions.runs.status": "Estado", "actions.runs.actors_no_select": "Todos los actores", "actions.runs.status_no_select": "Todo el estado", + "actions.runs.branch": "Rama", + "actions.runs.branches_no_select": "Todas las ramas", "actions.runs.no_results": "No hay resultados coincidentes.", "actions.runs.no_runs": "El flujo de trabajo no tiene ejecuciones todavía.", + "actions.runs.summary": "Resumen", "actions.workflow.disable": "Desactivar fllujo de trabajo", "actions.workflow.disable_success": "Flujo de trabajo '%s' deshabilitado correctamente.", "actions.workflow.enable": "Activar flujo de trabajo", "actions.workflow.enable_success": "Flujo de trabajo '%s' habilitado con éxito.", "actions.workflow.disabled": "El flujo de trabajo está deshabilitado.", + "actions.workflow.scope_owner": "Propietario", + "actions.workflow.required": "Obligatorio", "actions.need_approval_desc": "Necesita aprobación para ejecutar flujos de trabajo para el pull request del fork.", "actions.variables.management": "Gestión de variables", "actions.variables.creation": "Añadir variable", @@ -2956,8 +2928,6 @@ "projects.type-2.display_name": "Proyecto repositorio", "projects.type-3.display_name": "Proyecto de organización", "git.filemode.directory": "Directorio", - "git.filemode.normal_file": "Archivo normal", - "git.filemode.executable_file": "Archivo ejecutable", - "git.filemode.symbolic_link": "Enlace simbólico", - "git.filemode.submodule": "Submódulo" + "git.filemode.submodule": "Submódulo", + "actions.general.token_permissions.mode.restricted": "Restringido" } diff --git a/options/locale/locale_fa-IR.json b/options/locale/locale_fa-IR.json index a00d6dd5ca..ec9f1f833e 100644 --- a/options/locale/locale_fa-IR.json +++ b/options/locale/locale_fa-IR.json @@ -140,12 +140,6 @@ "install.register_confirm": "نیاز به تایید ایمیل ثبت نام", "install.mail_notify": "فعال‌سازی اعلان‌های ایمیل", "install.server_service_title": "تنظیمات سرور و سرویس‌های شخص ثالث", - "install.offline_mode": "فعال کردن حالت محلی", - "install.offline_mode_popup": "غیر فعال کردن شبکه های شخص ثالث تحویل محتوا و استفاده از تمام منابع به صورت محلی.", - "install.disable_gravatar": "غیر فعال کردن Gravatar", - "install.disable_gravatar_popup": "غیر فعال کردن کلیک و منابع آواتار شخص ثالث. مگر در مواردی که کاربر محلی بارگزاری آواتار پیش فرض استفاده خواهد شد.", - "install.federated_avatar_lookup": "فعال سازی آواتار مشترک", - "install.federated_avatar_lookup_popup": "مراجعه مشترک آواتار با استفاده از Libravatar را قادر می سازد.", "install.disable_registration": "غیرفعال‌کردن خود ثبت نامی", "install.disable_registration_popup": "غیرفعال کردن ثبت نام کاربر. تنها مدیر ها قادر خواهند بود حساب کاربری جدید اضافه کنند.", "install.allow_only_external_registration_popup": "اجازه ثبت نام فقط از طریق خدمات خارجی", @@ -164,11 +158,9 @@ "install.admin_email": "نشانی رایانامه (ایمیل)", "install.install_btn_confirm": "نصب گیتی", "install.test_git_failed": "عدم توانایی در آزمایش دستور 'git' توضیح بیشتر: %v", - "install.sqlite3_not_available": "نسخه مورد استفاده شما از SQLite3 پشتیبانی نمی کند. لطفا نسخه باینری رسمی را از s% دانلود کنید و از ورژن gobuild هم استفاده نکنید.", "install.invalid_db_setting": "تنظیمات پایگاه داده معتبر نیست: %v", "install.invalid_repo_path": "مسیر ریشه مخزن نامعتبر است: %v", "install.invalid_app_data_path": "مسیر داده برنامه نامعتبر است: %v", - "install.run_user_not_match": "نام کاربری 'اجرا به عنوان' نام کاربری فعلی نیست: %s -> %s", "install.internal_token_failed": "کد داخلی ایجاد نشد: %v", "install.secret_key_failed": "کلید مخفی ایجاد نشد: %v", "install.save_config_failed": "تنظیمات ذخیره نشد: %v", @@ -533,7 +525,6 @@ "settings.permission_read": "خواندن", "settings.manage_oauth2_applications": "مدیریت برنامه‎های OAuth2", "settings.edit_oauth2_application": "ویرایش برنامه OAuth2", - "settings.oauth2_applications_desc": "برنامه‎های OAuth2 احراز هویت برنامه های شخص ثالث را با بستری امن میسر می‎کند.", "settings.remove_oauth2_application": "حذف برنامه OAuth2", "settings.remove_oauth2_application_desc": "حذف برنامه OAuth2 دسترسی تمام برنامه های متصل با آن را از بین می‎برد. آیا ادامه می‎دهید؟", "settings.remove_oauth2_application_success": "برنامه حذف شده است.", @@ -546,7 +537,6 @@ "settings.oauth2_regenerate_secret": "تولید دوباره کلمه امن", "settings.oauth2_regenerate_secret_hint": "کلمه امن خود را فراموش کرده اید؟", "settings.oauth2_application_edit": "ويرايش", - "settings.oauth2_application_create_description": "برنامه‎های OAuth2 این امکان را به برنامه‎های شخص ثالث می‎دهد یک نمونه از دسترسی به حساب کاربری شما داشته باشد.", "settings.authorized_oauth2_applications": "برنامه OAuth2 تایید شد", "settings.revoke_key": "ابطال", "settings.revoke_oauth2_grant": "ابطال دسترسی", @@ -593,7 +583,6 @@ "repo.visibility": "پدیداری", "repo.visibility_description": "فقط مالک یا عضو سازمان اگر دسترسی داشته باشند. میتوانند این را مشاهده کنند.", "repo.visibility_helper_forced": "مدیر سایت شما ایجاد مخازن جدید را به صورت خصوصی اجباری کرده است.", - "repo.visibility_fork_helper": "(تغییر این مقدار تمام انشعاب‎ها را تحت تاثیر می گذارد)", "repo.clone_helper": "کمک برای نسخه‎برداری کمک.", "repo.fork_repo": "انشعاب از مخزن", "repo.fork_from": "انشعاب از", @@ -805,12 +794,10 @@ "repo.editor.no_changes_to_show": "تغییری برای نمایش وجود ندارد.", "repo.editor.push_rejected_summary": "متن کامل پیام دلیل رد شدن:", "repo.editor.add_subdir": "افزودن پوشه…", - "repo.editor.no_commit_to_branch": "نمی‌توان به طور مستقیم درمورد شاخه نطر داد زیرا:", "repo.editor.user_no_push_to_branch": "کاربر نمیتواند به شاخه ارسال کند", "repo.editor.require_signed_commit": "شاخه یک کامیت امضا شده لازم دارد", "repo.commits.desc": "تاریخچه تغییرات کد منبع را مرور کنید.", "repo.commits.commits": "کامیت‌ها", - "repo.commits.nothing_to_compare": "این شاخه ها برابرند.", "repo.commits.search_all": "همه شاخه ها", "repo.commits.author": "مولف", "repo.commits.message": "پیام", @@ -855,11 +842,9 @@ "repo.issues.new": "مسئله‌ی جدید", "repo.issues.new.title_empty": "عنوان نمی تواند خالی باشد", "repo.issues.new.labels": "برچسب‌ها", - "repo.issues.new.no_label": "بدون برچسب", "repo.issues.new.clear_labels": "پاک‌کردن برچسب‌ها", "repo.issues.new.projects": "پروژه‌ها", "repo.issues.new.clear_projects": "پاک کردن پروژه‌ها", - "repo.issues.new.no_projects": "هیچ پروژه ثبت نشده", "repo.issues.new.open_projects": "برنامه‌های باز", "repo.issues.new.closed_projects": "پروژه‌های بسته شده", "repo.issues.new.no_items": "موردی وجود ندارد", @@ -952,7 +937,6 @@ "repo.issues.context.reference_issue": "مرجع در شماره جدید", "repo.issues.context.edit": "ویرایش", "repo.issues.context.delete": "حذف", - "repo.issues.reopen_issue": "بازگشایی", "repo.issues.create_comment": "دیدگاه", "repo.issues.closed_at": "%[2]s این موضوع را بست", "repo.issues.reopened_at": "%[2]s این موضوع را دوباره باز کرد", @@ -1096,13 +1080,11 @@ "repo.pulls.desc": "نمایش تقاضای واکشی ها و بازبینی های کد.", "repo.pulls.new": "ایجاد تقاضای واکشی", "repo.pulls.compare_changes": "تقاضای واکشی جدید", - "repo.pulls.compare_changes_desc": "یک شاخه را برای ادغام با شاخه مورد نظر انتخاب تا از آن واکشی کنید.", "repo.pulls.compare_base": "ادغام با", "repo.pulls.compare_compare": "واکشی از", "repo.pulls.switch_comparison_type": "سوئیچ نوع مقایسه", "repo.pulls.switch_head_and_base": "سر و پایه سوئیچ", "repo.pulls.filter_branch": "صافی شاخه", - "repo.pulls.nothing_to_compare": "این شاخه‎ها یکی هستند. نیازی به تقاضای واکشی نیست.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "این شاخه ها برابر هستند. این PR خالی خواهد بود.", "repo.pulls.has_pull_request": "A درخواست pull بین این شاخه ها از قبل وجود دارد: %[2]s#%[3]d", "repo.pulls.create": "ایجاد تقاضای واکشی", @@ -1155,7 +1137,6 @@ "repo.pulls.status_checking": "برخی از بررسی‎ها در حال تعلیق هستند", "repo.pulls.status_checks_success": "تمامی بررسی‎ها موفق بودند", "repo.pulls.status_checks_warning": "برخی از چک ها هشدارهایی را گزارش کردند", - "repo.pulls.status_checks_failure": "برخی از چک ها موفق نبود", "repo.pulls.status_checks_error": "برخی از چک ها خطاهایی را گزارش کردند", "repo.pulls.status_checks_requested": "الزامی", "repo.pulls.status_checks_details": "جزئیات", @@ -1342,7 +1323,6 @@ "repo.settings.transfer_abort": "لغو انتقال", "repo.settings.transfer_abort_invalid": "شما نمی توانید یک انتقال انبار موجود را لغو کنید.", "repo.settings.transfer_desc": "انتقال مالکیت این مخزن به کاربر بانی یا سازمانی که شما حق مدیریت در آن دارید.", - "repo.settings.transfer_form_title": "نام مخزن را برای تایید عمل خورد اینجا وارد کنید:", "repo.settings.transfer_notices_1": "- شما دسترسی خود را نسبت مخزن را از دست میدهید اگر مالکیت آن را به یک کاربری مفرد انتقال دهید.", "repo.settings.transfer_notices_2": "- شما دسترسی خود را نسبت مخزن را حفظ میکنید. اگر مالکیت آن را به یک سازمانی که در آن مالکیت دارید انتقال دهید.", "repo.settings.transfer_notices_3": "- اگر انبار خصوصی است و به یک کاربر جداگانه منتقل می شود، این عمل مطمئن می شود که کاربر حداقل مجوز خواندن را دارد (و در صورت لزوم مجوزها را تغییر می دهد).", @@ -1396,8 +1376,6 @@ "repo.settings.webhook.payload": "محتوا", "repo.settings.webhook.body": "پیکر", "repo.settings.githook_edit_desc": "در صورتیکه hook غیرفعال باشد، محتوای نمونه ای موجود در آن ارائه خواهد شد. برای اینکه به کلی غیر فعال شود، محتوا را پاک کنید تا خالی شود.", - "repo.settings.githook_name": "نام hook", - "repo.settings.githook_content": "محتوای هوک", "repo.settings.update_githook": "به روزرسانی hook", "repo.settings.add_webhook_desc": "Gitea یک درخواست POST با نوع محتوی مشخص برای نشانی مقصذ ارسال خواهد کرد. برای کسب اطلاعات بیشتر به راهنمای هوک تحت وب مراجعه کنید.", "repo.settings.payload_url": "نشانی هدف", @@ -1502,7 +1480,6 @@ "repo.settings.block_on_official_review_requests_desc": "زمانی که درخواست‌های بررسی رسمی داشته باشد، ادغام امکان‌پذیر نخواهد بود، حتی اگر تأییدیه‌های کافی وجود داشته باشد.", "repo.settings.block_outdated_branch": "اگر درخواست pull قدیمی است، ادغام را مسدود شود", "repo.settings.block_outdated_branch_desc": "وقتی سرشاخه پشت شاخه پایه باشد، ادغام امکان پذیر نخواهد بود.", - "repo.settings.default_branch_desc": "شاخه اصلی مخزن را برای تقاضا واکشی و کامیت کد ها انتخاب نمایید:", "repo.settings.choose_branch": "شاخه اصلی مخزن را برای تقاضا واکشی و کامیت کد ها انتخاب نمایید:", "repo.settings.no_protected_branch": "اینجا هیچ شاخه محافظت شده ای وجود ندارد.", "repo.settings.edit_protected_branch": "ویرایش", @@ -1586,8 +1563,6 @@ "repo.diff.too_many_files": "برخی از فایل ها نشان داده نشدند زیرا تعداد زیادی فایل در این تفاوت تغییر کرده اند", "repo.diff.show_more": "نمایش بیشتر", "repo.diff.load": "Diff را بارگزاری کن", - "repo.diff.generated": "تولید شده", - "repo.diff.vendored": "فروخته شده", "repo.diff.comment.placeholder": "اظهار نظر کنید", "repo.diff.comment.add_single_comment": "افزودن یک دیدگاه به تنهایی", "repo.diff.comment.add_review_comment": "افزودن دیدگاه", @@ -1600,6 +1575,7 @@ "repo.diff.review.approve": "پذیرفتن", "repo.diff.review.reject": "درخواست تغییر کوکی ها", "repo.diff.committed_by": "کامیت شده توسط", + "repo.commits.avatar_stack_and": "و", "repo.diff.protected": "حفاظت شده", "repo.diff.image.side_by_side": "کنار هم", "repo.diff.image.swipe": "با خط کشش", @@ -1706,9 +1682,7 @@ "org.settings.labels_desc": "تگ هایی را اضافه کنید که می‌توانند برای مشکلات همه مخازن تحت این سازمان استفاده شوند.", "org.members.membership_visibility": "قابل مشاهده بودن عضویت:", "org.members.public": "نمایان", - "org.members.public_helper": "مخفی کردن", "org.members.private": "مخفی", - "org.members.private_helper": "نمایان کردن", "org.members.member_role": "نقش عضو:", "org.members.owner": "مالک", "org.members.member": "عضو", @@ -1735,7 +1709,6 @@ "org.teams.members": "اعضای تیم", "org.teams.update_settings": "به‌ روزرسانی تنظیمات", "org.teams.delete_team": "حذف تیم", - "org.teams.add_team_member": "افزودن عضو به تیم", "org.teams.delete_team_title": "حذف تیم", "org.teams.delete_team_desc": "حذف یک تیم از مخزن تمامی دسترسی اعضای آن را از بین می‌برد. آیا مطمئن هستید؟", "org.teams.delete_team_success": "تیم حذف شد.", @@ -1758,12 +1731,17 @@ "org.teams.all_repositories_read_permission_desc": "این تیم دسترسی خواندن مخازن همه را می بخشد: اعضا می توانند مخازن را مشاهده و همزاد سازی کنند.", "org.teams.all_repositories_write_permission_desc": "این تیم دسترسی نوشتن مخازن همه را می بخشد: اعضا می توانند مخازن را مشاهده و درج کنند.", "org.teams.all_repositories_admin_permission_desc": "این تیم دسترسی مدیر به مخازن همه را می بخشد: اعضا می توانند مخازن را بخواند، همکار و مخزن اضافه کنند.", + "org.teams.visibility": "پدیداری", + "org.teams.visibility_private": "خصوصی", + "org.teams.visibility_limited": "محدود شده", + "org.teams.visibility_public": "عمومی", "admin.dashboard": "پیشخوان", "admin.users": "حساب کاربران", "admin.organizations": "تشکیلات", "admin.repositories": "مخازن", "admin.hooks": "وب هوک ها", "admin.authentication": "منابع احراز هویت", + "admin.badges.description": "توضیحات", "admin.config": "پیکربندی", "admin.config_summary": "چکیده", "admin.config_settings": "تنظيمات", @@ -1993,11 +1971,8 @@ "admin.config.server_config": "پیکربندی سرور", "admin.config.app_name": "عنوان سایت", "admin.config.app_ver": "نسخه‌ی GitGo", - "admin.config.app_url": "آدرس پایه گیتی", "admin.config.custom_conf": "مسیر پرونده پیکربندی", "admin.config.custom_file_root_path": "مسیر ریشه‌ی پرونده سفارشی", - "admin.config.domain": "دامنه سرور", - "admin.config.offline_mode": "شیوه محلی", "admin.config.disable_router_log": "غیرفعال کردن گزارش مسیریاب", "admin.config.run_user": "اجرا به عنوان نام کاربری", "admin.config.run_mode": "حالت اجرا", @@ -2046,7 +2021,6 @@ "admin.config.default_enable_timetracking": "فعال سازی پیگیری زمان به صورت پیش فرض", "admin.config.default_allow_only_contributors_to_track_time": "اجاز پگیری زمان مشارکت فقط", "admin.config.no_reply_address": "مخفی کردن دامنه ایمیل", - "admin.config.default_visibility_organization": "وضعیت پیشفرض پدیداری برای سازمان جدید", "admin.config.default_enable_dependencies": "فعال کردن وابستگی پیشفرض برای مسئله", "admin.config.webhook_config": "پیکربندی هوک تحت وب", "admin.config.queue_length": "طول صف", @@ -2066,11 +2040,9 @@ "admin.config.cache_config": "پیکربندی حافظه پنهان", "admin.config.cache_adapter": "وفق دهنده حافظه پنهان", "admin.config.cache_interval": "وقفه حافظه نهان", - "admin.config.cache_conn": "اتصال حافظه نهان", "admin.config.cache_item_ttl": "مولفه TTL حافظه نهان", "admin.config.session_config": "پیکربندی نشست ها", "admin.config.session_provider": "تامین کننده نشست", - "admin.config.provider_config": "پیکربندی تامین کننده", "admin.config.cookie_name": "نام کوکی", "admin.config.gc_interval_time": "فاصله زمانی GC", "admin.config.session_life_time": "طول عمر نشست", @@ -2078,7 +2050,6 @@ "admin.config.cookie_life_time": "طول عمر کوکی", "admin.config.picture_config": "پیکربندی عکس و آواتار", "admin.config.picture_service": "سرویس تصویر", - "admin.config.disable_gravatar": "غیر فعال کردن Gravatar", "admin.config.enable_federated_avatar": "فعال سازی آواتار مشترک", "admin.config.git_config": "پیکربندی Git", "admin.config.git_disable_diff_highlight": "غیرفعال کردن برجسته سازی در Diff", @@ -2088,8 +2059,6 @@ "admin.config.git_gc_args": "آرگومان های GC", "admin.config.git_migrate_timeout": "آستانه ی زمان مهاجرت", "admin.config.git_mirror_timeout": "زمان آستانه در به روز رسانی قرینه", - "admin.config.git_clone_timeout": "زمان آستانه ی عملیات Clone", - "admin.config.git_pull_timeout": "زمان آستانه ی عملیات واکشی", "admin.config.git_gc_timeout": "زمان آستانه ی عملیات GC", "admin.config.log_config": "پیکربندی ثبت رخداد", "admin.config.disabled_logger": "غیرفعال شده", @@ -2216,6 +2185,8 @@ "packages.container.labels": "برچسب‌ها", "packages.npm.dependencies": "وابستگی ها", "packages.npm.details.tag": "برچسب", + "packages.terraform.lock": "قفل کردن", + "packages.terraform.unlock": "رفع انسداد", "packages.owner.settings.cleanuprules.enabled": "فعال شده", "secrets.creation.description": "شرح", "actions.runners.name": "نام", @@ -2228,5 +2199,8 @@ "actions.runners.status.active": "فعال", "actions.runners.version": "نسخه", "actions.runs.commit": "کامیت", - "git.filemode.symbolic_link": "پیوند نمادین" + "actions.runs.summary": "چکیده", + "actions.workflow.scope_owner": "مالک", + "actions.workflow.required": "الزامی", + "actions.general.token_permissions.mode.restricted": "محصور" } diff --git a/options/locale/locale_fi-FI.json b/options/locale/locale_fi-FI.json index 2ea42ddd31..3a413bbcfe 100644 --- a/options/locale/locale_fi-FI.json +++ b/options/locale/locale_fi-FI.json @@ -153,12 +153,6 @@ "install.register_confirm": "Vaadi sähköpostin vahvistaminen rekisteröintiin", "install.mail_notify": "Ota käyttöön sähköpostiilmoitukset", "install.server_service_title": "Palvelin ja kolmansien osapuolten palveluiden asetukset", - "install.offline_mode": "Ota käyttöön lokaali tila", - "install.offline_mode_popup": "Poista kolmannen osapuolen sisällöstä jakeluverkot ja tarjoa kaikki resurssit paikallisesti.", - "install.disable_gravatar": "Poista Gravatar käytöstä", - "install.disable_gravatar_popup": "Poista Gravatar ja kolmannen osapuolen avaratir käytöstä. Oletus-avatar näytetään, ellei käyttäjä ole ladannut omaansa.", - "install.federated_avatar_lookup": "Käytä ulkopuolisia profiilikuvia", - "install.federated_avatar_lookup_popup": "Enable federated avatars lookup to use federated open source service based on libravatar.", "install.disable_registration": "Poista rekisteröinti käytöstä", "install.disable_registration_popup": "Poista käyttäjän itse-rekisteröinti, vain ylläpito voi luoda tilejä.", "install.allow_only_external_registration_popup": "Salli rekisteröinti vain ulkopuolisista palveluista", @@ -177,7 +171,6 @@ "install.admin_email": "Sähköpostiosoite", "install.install_btn_confirm": "Asenna Gitea", "install.test_git_failed": "Epäonnistui testata 'git' komentoa: %v", - "install.sqlite3_not_available": "Tämä Gitea versio ei tue SQLite3. Lataa virallinen binääriversio kohteesta %s (ei 'gobuild' versio).", "install.invalid_db_setting": "Tietokanta-asetukset ovat väärin: %v", "install.invalid_repo_path": "Repojen juuri polku on virheellinen: %v", "install.invalid_app_data_path": "Sovelluksen datapolku on virheellinen: %v", @@ -632,7 +625,6 @@ "repo.editor.add_subdir": "Lisää hakemisto…", "repo.editor.require_signed_commit": "Haara vaatii vahvistetun commitin", "repo.commits.commits": "Commitit", - "repo.commits.nothing_to_compare": "Nämä haarat vastaavat toisiaan.", "repo.commits.search_all": "Kaikki haarat", "repo.commits.author": "Tekijä", "repo.commits.message": "Viesti", @@ -663,7 +655,6 @@ "repo.issues.filter_milestones": "Suodata merkkipaalu", "repo.issues.new": "Uusi ongelma", "repo.issues.new.labels": "Tunnisteet", - "repo.issues.new.no_label": "Ei tunnistetta", "repo.issues.new.clear_labels": "Tyhjennä tunnisteet", "repo.issues.new.projects": "Projektit", "repo.issues.new.no_items": "Ei kohteita", @@ -736,7 +727,6 @@ "repo.issues.context.reference_issue": "Viittaa uudesa ongelmassa", "repo.issues.context.edit": "Muokkaa", "repo.issues.context.delete": "Poista", - "repo.issues.reopen_issue": "Avaa uudelleen", "repo.issues.create_comment": "Kommentoi", "repo.issues.closed_at": "sulki tämän ongelman %[2]s", "repo.issues.reopened_at": "uudelleenavasi tämän ongelman %[2]s", @@ -814,7 +804,6 @@ "repo.pulls.viewed_files_label": "%[1]d / %[2]d tiedostoa katsottu", "repo.pulls.compare_compare": "vedä kohteesta", "repo.pulls.filter_branch": "Suodata branch", - "repo.pulls.nothing_to_compare": "Nämä haarat vastaavat toisiaan. Ei ole tarvetta luoda vetopyyntöä.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "Nämä haarat vastaavat toisiaan. Vetopyyntö tulee olemaan tyhjä.", "repo.pulls.has_pull_request": "Vetopyyntö haarojen välillä on jo olemassa: %[2]s#%[3]d", "repo.pulls.create": "Luo Pull-pyyntö", @@ -917,7 +906,6 @@ "repo.settings.danger_zone": "Vaaravyöhyke", "repo.settings.new_owner_has_same_repo": "Uudella omistajalla on jo samanniminen repo. Ole hyvä ja valitse toinen nimi.", "repo.settings.transfer": "Siirrä omistajuus", - "repo.settings.transfer_form_title": "Syötä repon nimi vahvistuksena:", "repo.settings.transfer_notices_3": "- Jos arkisto on yksityinen ja se siirretään yksittäiselle käyttäjälle, tämä toiminto varmistaa, että käyttäjällä on ainakin lukuoikeudet (ja muuttaa käyttöoikeuksia tarvittaessa).", "repo.settings.transfer_owner": "Uusi omistaja", "repo.settings.wiki_delete": "Poista Wiki data", @@ -939,8 +927,6 @@ "repo.settings.webhook.payload": "Sisältö", "repo.settings.webhook.body": "Sisältö", "repo.settings.githook_edit_desc": "Jos koukku ei ole käytössä, esitellään esimerkkisisältö. Sisällön jättäminen tyhjäksi arvoksi poistaa tämän koukun käytöstä.", - "repo.settings.githook_name": "Koukun nimi", - "repo.settings.githook_content": "Koukun sisältö", "repo.settings.update_githook": "Päivitys koukku", "repo.settings.payload_url": "Kohde URL", "repo.settings.http_method": "HTTP-menetelmä", @@ -1060,6 +1046,7 @@ "repo.diff.review.comment": "Kommentoi", "repo.diff.review.approve": "Hyväksy", "repo.diff.review.reject": "Pyydä muutoksia", + "repo.commits.avatar_stack_and": "ja", "repo.release.releases": "Julkaisut", "repo.release.tags": "Tagit", "repo.release.new_release": "Uusi julkaisu", @@ -1130,9 +1117,7 @@ "org.settings.hooks_desc": "Lisää webkoukkuja, jotka suoritetaan kaikissa repoissa tässä organisaatiossa.", "org.members.membership_visibility": "Jäsenyyden näkyvyys:", "org.members.public": "Näkyvä", - "org.members.public_helper": "tee piilotettu", "org.members.private": "Piilotettu", - "org.members.private_helper": "tee näkyvä", "org.members.member_role": "Jäsenen rooli:", "org.members.owner": "Omistaja", "org.members.member": "Jäsen", @@ -1154,7 +1139,6 @@ "org.teams.members": "Ryhmän jäsenet", "org.teams.update_settings": "Päivitä asetukset", "org.teams.delete_team": "Poista tiimi", - "org.teams.add_team_member": "Lisää tiimin jäsen", "org.teams.delete_team_title": "Tiimin poisto", "org.teams.delete_team_desc": "Tiimin poisto peruuttaa sen jäseniltä oikeuden päästä tiimin varastoihin. Jatketaanko?", "org.teams.delete_team_success": "Tiimi on poistettu.", @@ -1164,12 +1148,17 @@ "org.teams.repositories": "Tiimin repot", "org.teams.members.none": "Ei jäseniä tässä tiimissä.", "org.teams.all_repositories": "Kaikki repot", + "org.teams.visibility": "Näkyvyys", + "org.teams.visibility_private": "Yksityinen", + "org.teams.visibility_limited": "Rajattu", + "org.teams.visibility_public": "Julkinen", "admin.dashboard": "Kojelauta", "admin.users": "Käyttäjätilit", "admin.organizations": "Organisaatiot", "admin.repositories": "Repot", "admin.hooks": "Webkoukut", "admin.authentication": "Todennuslähteet", + "admin.badges.description": "Kuvaus", "admin.config": "Asetukset", "admin.config_summary": "Yhteenveto", "admin.config_settings": "Asetukset", @@ -1313,8 +1302,6 @@ "admin.config.server_config": "Palvelin asetukset", "admin.config.app_name": "Sivuston otsikko", "admin.config.app_ver": "Gitea versio", - "admin.config.app_url": "Gitean juuriosoite", - "admin.config.domain": "Palvelimen verkkotunnus", "admin.config.disable_router_log": "Poista käytöstä reitittimen loki", "admin.config.run_user": "Aja käyttäjänä", "admin.config.run_mode": "Suoritustila", @@ -1353,7 +1340,6 @@ "admin.config.enable_timetracking": "Ota käyttöön ajan seuranta", "admin.config.default_enable_timetracking": "Ota ajan seuranta oletusarvoisesti käyttöön", "admin.config.no_reply_address": "Piilotettu sähköpostin verkkotunnus", - "admin.config.default_visibility_organization": "Uuden organisaation oletusnäkyvyys", "admin.config.webhook_config": "Webkoukku asetukset", "admin.config.queue_length": "Jonon pituus", "admin.config.deliver_timeout": "Toimitus aikakatkaisu", @@ -1367,22 +1353,18 @@ "admin.config.cache_config": "Välimuistin asetukset", "admin.config.cache_adapter": "Välimuistin sovitin", "admin.config.cache_interval": "Välimuistin aikaväli", - "admin.config.cache_conn": "Välimuistin yhteys merkkijono", "admin.config.session_config": "Istunnon asetukset", "admin.config.session_provider": "Istunnon toimittaja", - "admin.config.provider_config": "Toimittajan asetukset", "admin.config.cookie_name": "Evästenimi", "admin.config.gc_interval_time": "GC aikaväli aika", "admin.config.session_life_time": "Istunnon elinikä", "admin.config.https_only": "Vain HTTPS", "admin.config.cookie_life_time": "Evästeen elinikä", "admin.config.picture_service": "Kuva palvelu", - "admin.config.disable_gravatar": "Poista käytöstä Gravatar", "admin.config.enable_federated_avatar": "Käytä ulkopuolisia profiilikuvia", "admin.config.git_gc_args": "Roskienkeruun parametrit", "admin.config.git_migrate_timeout": "Migraatio aikakatkaistiin", "admin.config.git_mirror_timeout": "Peilauspäivitys aikakatkaistiin", - "admin.config.git_clone_timeout": "Kloonaus aikakatkaistiin", "admin.config.git_gc_timeout": "Roskienkeruu aikakatkaistiin", "admin.config.log_config": "Loki asetukset", "admin.config.disabled_logger": "Pois käytöstä", @@ -1466,6 +1448,9 @@ "packages.container.labels": "Tunnisteet", "packages.npm.dependencies": "Riippuvuudet", "packages.npm.details.tag": "Tagi", + "packages.terraform.lock_status": "Lukituksen tila", + "packages.terraform.lock": "Lukitse", + "packages.terraform.unlock": "Avaa", "packages.owner.settings.cleanuprules.enabled": "Käytössä", "secrets.creation.description": "Kuvaus", "actions.runners.name": "Nimi", @@ -1474,5 +1459,9 @@ "actions.runners.labels": "Tunnisteet", "actions.runners.task_list.run": "Suorita", "actions.runners.task_list.repository": "Repo", - "actions.runners.version": "Versio" + "actions.runners.version": "Versio", + "actions.runs.branch": "Haara", + "actions.runs.summary": "Yhteenveto", + "actions.workflow.scope_owner": "Omistaja", + "actions.general.token_permissions.mode.restricted": "Rajoitettu" } diff --git a/options/locale/locale_fr-FR.json b/options/locale/locale_fr-FR.json index 5cfa7498b9..f138a7e979 100644 --- a/options/locale/locale_fr-FR.json +++ b/options/locale/locale_fr-FR.json @@ -10,7 +10,7 @@ "sign_out": "Déconnexion", "sign_up": "S'inscrire", "link_account": "Lier un Compte", - "register": "S'inscrire", + "register": "S’inscrire", "version": "Version", "powered_by": "Propulsé par %s", "page": "Page", @@ -80,8 +80,8 @@ "cancel": "Annuler", "retry": "Réessayez", "rerun": "Relancer", - "rerun_all": "Relancer toutes les tâches", - "rerun_failed": "Relancer les tâches échouées", + "rerun_all": "Relancer toutes les missions.", + "rerun_failed": "Relancer les missions échouées.", "save": "Enregistrer", "add": "Ajouter", "add_all": "Tout Ajouter", @@ -165,7 +165,7 @@ "search.fuzzy_tooltip": "Inclure également les résultats proches de la recherche", "search.words": "Mots", "search.words_tooltip": "Inclure uniquement les résultats qui correspondent exactement aux mots recherchés", - "search.regexp": "Regexp", + "search.regexp": "Expression régulière", "search.regexp_tooltip": "Inclure uniquement les résultats qui correspondent à l’expression régulière recherchée", "search.exact": "Exact", "search.exact_tooltip": "Inclure uniquement les résultats qui correspondent exactement au terme de recherche", @@ -185,7 +185,7 @@ "search.tag_kind": "Chercher des étiquettes…", "search.tag_tooltip": "Cherchez des étiquettes correspondantes. Utilisez « % » pour rechercher n’importe quelle suite de nombres.", "search.commit_kind": "Chercher des révisions…", - "search.runner_kind": "Chercher des exécuteurs…", + "search.runner_kind": "Chercher des opérateurs…", "search.no_results": "Aucun résultat correspondant trouvé.", "search.issue_kind": "Recherche de tickets…", "search.pull_kind": "Recherche de demandes d’ajouts…", @@ -289,7 +289,7 @@ "install.smtp_port": "Port SMTP", "install.smtp_from": "Envoyer les courriels en tant que", "install.smtp_from_invalid": "L’adresse « Envoyer le courriel sous » est invalide", - "install.smtp_from_helper": "Adresse courriel utilisée par Gitea. Utilisez directement votre adresse ou la forme « Nom  ».", + "install.smtp_from_helper": "Adresse courriel utilisée par Gitea. Utilisez directement votre adresse ou la forme « Nom  ».", "install.mailer_user": "Utilisateur SMTP", "install.mailer_password": "Mot de passe SMTP", "install.register_confirm": "Exiger la confirmation du courriel lors de l’inscription", @@ -308,7 +308,7 @@ "install.require_sign_in_view_popup": "Limiter l’accès aux pages aux utilisateurs connectés. Les visiteurs ne verront que les pages de connexion et d’inscription.", "install.admin_setting_desc": "La création d'un compte administrateur est facultative. Le premier utilisateur enregistré deviendra automatiquement un administrateur le cas échéant.", "install.admin_title": "Paramètres de compte administrateur", - "install.admin_name": "Nom d’utilisateur administrateur", + "install.admin_name": "Nom de l’administrateur", "install.admin_password": "Mot de passe", "install.confirm_password": "Confirmez le mot de passe", "install.admin_email": "Courriel", @@ -505,10 +505,10 @@ "mail.repo.actions.run.failed": "L’exécution a échoué", "mail.repo.actions.run.succeeded": "L’exécution a réussi", "mail.repo.actions.run.cancelled": "L’exécution a été annulée", - "mail.repo.actions.jobs.all_succeeded": "Tous les tâches ont réussi.", - "mail.repo.actions.jobs.all_failed": "Toutes les tâches ont échoué.", - "mail.repo.actions.jobs.some_not_successful": "Certaines tâches n’ont pas réussi.", - "mail.repo.actions.jobs.all_cancelled": "Toutes les tâches ont bien été annulés.", + "mail.repo.actions.jobs.all_succeeded": "Tous les missions ont réussi.", + "mail.repo.actions.jobs.all_failed": "Toutes les missions ont échoué.", + "mail.repo.actions.jobs.some_not_successful": "Certaines missions n’ont pas réussi.", + "mail.repo.actions.jobs.all_cancelled": "Toutes les missions ont bien été annulées.", "mail.team_invite.subject": "%[1]s vous a invité à rejoindre l’organisation %[2]s", "mail.team_invite.text_1": "%[1]s vous a invité à rejoindre l’équipe %[2]s dans l’organisation %[3]s.", "mail.team_invite.text_2": "Veuillez cliquer sur le lien suivant pour rejoindre l'équipe :", @@ -916,7 +916,7 @@ "settings.passcode_invalid": "Le mot de passe est invalide. Réessayez.", "settings.twofa_enrolled": "L’authentification à deux facteurs a été activée pour votre compte. Gardez votre clé de secours (%s) en lieu sûr, car il ne vous sera montré qu'une seule fois.", "settings.twofa_failed_get_secret": "Impossible d'obtenir le secret.", - "settings.webauthn_desc": "Les clés de sécurité sont des dispositifs matériels contenant des clés cryptographiques. Elles peuvent être utilisées pour l’authentification à deux facteurs. La clé de sécurité doit supporter le standard WebAuthn Authenticator.", + "settings.webauthn_desc": "Les clés de sécurité sont des dispositifs matériels contenant des clés cryptographiques. Elles peuvent être utilisées pour l’authentification à deux facteurs. Gitea requière le support de l’API Web Authentication.", "settings.webauthn_register_key": "Ajouter une clé de sécurité", "settings.webauthn_nickname": "Pseudonyme", "settings.webauthn_delete_key": "Retirer la clé de sécurité", @@ -944,8 +944,8 @@ "settings.email_notifications.disable": "Ne pas notifier", "settings.email_notifications.submit": "Définir les préférences de courriel", "settings.email_notifications.andyourown": "Inclure vos propres notifications", - "settings.email_notifications.actions.desc": "Notification pour les executions de workflows sur les dépôts configurés avec les Actions Gitea.", - "settings.email_notifications.actions.failure_only": "Ne notifier que pour les exécutions échouées", + "settings.email_notifications.actions.desc": "Notifier les procédures des dépôts configurés avec les Actions Gitea.", + "settings.email_notifications.actions.failure_only": "Ne notifier que procédures échouées", "settings.visibility": "Visibilité de l'utilisateur", "settings.visibility.public": "Publique", "settings.visibility.public_tooltip": "Visible par tout le monde", @@ -992,7 +992,7 @@ "repo.repo_desc": "Description", "repo.repo_desc_helper": "Décrire brièvement votre dépôt", "repo.repo_no_desc": "Aucune description fournie", - "repo.repo_lang": "Langue", + "repo.repo_lang": "Langues", "repo.repo_gitignore_helper": "Sélectionner quelques .gitignore prédéfinies", "repo.repo_gitignore_helper_desc": "De nombreux outils et compilateurs génèrent des fichiers résiduels qui n'ont pas besoin d'être supervisés par git. Composez un .gitignore à l’aide de cette liste des languages de programmation courants.", "repo.issue_labels": "Jeu de labels pour les tickets", @@ -1041,7 +1041,7 @@ "repo.stars": "Favoris", "repo.reactions_more": "et %d de plus", "repo.reactions": "Réactions", - "repo.unit_disabled": "L'administrateur du site a désactivé cette section du dépôt.", + "repo.unit_disabled": "L’administrateur du site a désactivé cette section du dépôt.", "repo.language_other": "Autre", "repo.adopt_search": "Entrez un nom d’utilisateur pour rechercher les dépôts dépossédés… (laissez vide pour tous trouver)", "repo.adopt_preexisting_label": "Adopter les fichiers", @@ -1106,9 +1106,9 @@ "repo.migrate_items_merge_requests": "Demandes de fusion", "repo.migrate_items_releases": "Publications", "repo.migrate_repo": "Migrer le dépôt", - "repo.migrate.clone_address": "Migrer/Cloner depuis une URL", - "repo.migrate.clone_address_desc": "L'URL HTTP(S) ou Git \"clone\" d'un dépôt existant", - "repo.migrate.github_token_desc": "Vous pouvez mettre un ou plusieurs jetons séparés par des virgules ici pour rendre la migration plus rapide et contourner la limite de débit de l’API GitHub. ATTENTION : Abuser de cette fonctionnalité peut enfreindre la politique du fournisseur de service et entraîner un blocage de votre compte.", + "repo.migrate.clone_address": "Migrer depuis une URL", + "repo.migrate.clone_address_desc": "L’URL ou le lien « Git clone » d’un dépôt existant", + "repo.migrate.github_token_desc": "Vous pouvez mettre un ou plusieurs jetons séparés par des virgules ici pour rendre la migration plus rapide et contourner la limite de débit de l’API GitHub. Attention : abuser de cette fonctionnalité peut enfreindre la politique du fournisseur de service et entraîner un blocage de votre compte.", "repo.migrate.clone_local_path": "ou un chemin serveur local", "repo.migrate.permission_denied": "Vous n'êtes pas autorisé à importer des dépôts locaux.", "repo.migrate.permission_denied_blocked": "Vous ne pouvez pas importer depuis des domaines bannis, veuillez demander à votre administrateur de vérifier les paramètres ALLOWED_DOMAINS, ALLOW_LOCALNETWORKS ou BLOCKED_DOMAINS.", @@ -1165,7 +1165,7 @@ "repo.clone_this_repo": "Cloner ce dépôt", "repo.cite_this_repo": "Citer ce dépôt", "repo.create_new_repo_command": "Création d'un nouveau dépôt en ligne de commande", - "repo.push_exist_repo": "Soumission d'un dépôt existant par ligne de commande", + "repo.push_exist_repo": "Soumission d’un dépôt existant par ligne de commande", "repo.empty_message": "Ce dépôt n’a pas de contenu.", "repo.broken_message": "Les données git de ce dépôt ne peuvent pas être lues. Contactez l'administrateur de cette instance ou supprimez ce dépôt.", "repo.no_branch": "Ce dépôt n’a aucune branche.", @@ -1294,7 +1294,7 @@ "repo.editor.file_changed_while_editing": "Le contenu du fichier a changé depuis que vous avez commencé à éditer. Cliquez ici pour voir les changements ou soumettez de nouveau pour les écraser.", "repo.editor.file_already_exists": "Un fichier nommé \"%s\" existe déjà dans ce dépôt.", "repo.editor.commit_id_not_matching": "L’ID de la révision ne correspond pas à l’ID lorsque vous avez commencé à éditer. Faites une révision dans une branche de correctif puis fusionnez.", - "repo.editor.push_out_of_date": "Cet envoi semble être obsolète.", + "repo.editor.push_out_of_date": "Cette soumission semble être obsolète.", "repo.editor.commit_empty_file_header": "Réviser un fichier vide", "repo.editor.commit_empty_file_text": "Le fichier que vous allez réviser est vide. Continuer ?", "repo.editor.no_changes_to_show": "Il n’y a aucune modification à afficher.", @@ -1307,7 +1307,7 @@ "repo.editor.upload_files_to_dir": "Téléverser les fichiers vers \"%s\"", "repo.editor.cannot_commit_to_protected_branch": "Impossible de créer une révision sur la branche protégée \"%s\".", "repo.editor.no_commit_to_branch": "Impossible de réviser cette branche car :", - "repo.editor.user_no_push_to_branch": "L'utilisateur ne peut pas pousser vers la branche", + "repo.editor.user_no_push_to_branch": "L’utilisateur ne peut pas soumettre sur la branche", "repo.editor.require_signed_commit": "Cette branche nécessite une révision signée", "repo.editor.cherry_pick": "Picorer %s vers:", "repo.editor.revert": "Rétablir %s sur:", @@ -1317,10 +1317,11 @@ "repo.editor.fork_create_description": "Vous ne pouvez pas modifier ce dépôt directement. Cependant, vous pouvez bifurquer ce dépôt, et créer une demande d’ajout avec vos contributions.", "repo.editor.fork_edit_description": "Vous ne pouvez pas modifier ce dépôt directement. Les modifications seront écrites sur une bifurcation %s, vous permettant de faire une demande d’ajout.", "repo.editor.fork_not_editable": "Vous avez bifurqué ce dépôt mais votre copie n’est pas modifiable.", - "repo.editor.fork_failed_to_push_branch": "Impossible de pousser la branche %s vers votre dépôt.", + "repo.editor.fork_failed_to_push_branch": "Impossible de soumettre la branche %s vers votre dépôt.", "repo.editor.fork_branch_exists": "La branche « %s » existe déjà dans votre bifurcation, veuillez choisir un nouveau nom.", "repo.commits.desc": "Naviguer dans l'historique des modifications.", "repo.commits.commits": "Révisions", + "repo.commits.history_enable_follow_renames": "Inclure les renommages", "repo.commits.no_commits": "Pas de révisions en commun. \"%s\" et \"%s\" ont des historiques entièrement différents.", "repo.commits.nothing_to_compare": "Ces révisions sont équivalentes.", "repo.commits.search.tooltip": "Vous pouvez utiliser les mots-clés \"author:\", \"committer:\", \"after:\", ou \"before:\" pour filtrer votre recherche, ex.: \"revert author:Alice before:2019-01-13\".", @@ -1332,8 +1333,8 @@ "repo.commits.older": "Précédemment", "repo.commits.newer": "Récemment", "repo.commits.signed_by": "Signé par", - "repo.commits.signed_by_untrusted_user": "Signature provenant d'un utilisateur dilletant", - "repo.commits.signed_by_untrusted_user_unmatched": "Signature discordante de l'auteur de la révision et provenant d'un utilisateur dilletant", + "repo.commits.signed_by_untrusted_user": "Signé en dilettante par", + "repo.commits.signed_by_untrusted_user_unmatched": "Signé, sans en être l’auteur, par", "repo.commits.gpg_key_id": "ID de la clé GPG", "repo.commits.ssh_key_fingerprint": "Empreinte numérique de la clé SSH", "repo.commits.view_path": "Voir à ce point de l'historique", @@ -1621,7 +1622,7 @@ "repo.issues.lock.notice_2": "- Vous et les autres collaborateurs ayant accès à ce dépôt peuvent toujours laisser des commentaires que d’autres peuvent voir.", "repo.issues.lock.notice_3": "- Vous pouvez toujours déverrouiller ce ticket à l'avenir.", "repo.issues.unlock.notice_1": "- Tout le monde sera de nouveau en mesure de commenter ce ticket.", - "repo.issues.unlock.notice_2": "- Vous pouvez toujours verrouiller ce ticket à l'avenir.", + "repo.issues.unlock.notice_2": "- Vous pouvez toujours verrouiller ce ticket à l’avenir.", "repo.issues.lock.reason": "Motif de verrouillage", "repo.issues.lock.title": "Verrouiller la conversation sur ce ticket.", "repo.issues.unlock.title": "Déverrouiller la conversation sur ce ticket.", @@ -1817,9 +1818,9 @@ "repo.pulls.is_checking": "Recherche de conflits de fusion…", "repo.pulls.is_ancestor": "Cette branche est déjà présente dans la branche ciblée. Il n'y a rien à fusionner.", "repo.pulls.is_empty": "Les changements sur cette branche sont déjà sur la branche cible. Cette révision sera vide.", - "repo.pulls.required_status_check_failed": "Certains contrôles requis n'ont pas réussi.", - "repo.pulls.required_status_check_missing": "Certains contrôles requis sont manquants.", - "repo.pulls.required_status_check_administrator": "En tant qu'administrateur, vous pouvez toujours fusionner cette requête de pull.", + "repo.pulls.required_status_check_failed": "Certains signaux requis n'ont pas réussi.", + "repo.pulls.required_status_check_missing": "Certains signaux requis sont manquants.", + "repo.pulls.required_status_check_administrator": "En tant qu’administrateur, vous pouvez fusionner cette demande d’ajout.", "repo.pulls.required_status_check_bypass_allowlist": "Vous êtes autorisé à contourner les règles de protection pour cette fusion.", "repo.pulls.blocked_by_approvals": "Cette demande d’ajout n’est pas suffisamment approuvée. %d approbations obtenues sur %d.", "repo.pulls.blocked_by_approvals_whitelisted": "Cette demande d’ajout n’a pas encore assez d’approbations. %d sur %d approbations de la part des utilisateurs ou équipes sur la liste autorisée.", @@ -1843,7 +1844,7 @@ "repo.pulls.no_merge_desc": "Cette demande d’ajout ne peut être fusionnée car toutes les options de fusion du dépôt sont désactivées.", "repo.pulls.no_merge_helper": "Activez des options de fusion dans les paramètres du dépôt ou fusionnez la demande manuellement.", "repo.pulls.no_merge_wip": "Cette demande d’ajout ne peut pas être fusionnée car elle est marquée en chantier.", - "repo.pulls.no_merge_not_ready": "Cette demande d’ajout n’est pas prête à être fusionnée, vérifiez les évaluations et le contrôle qualité.", + "repo.pulls.no_merge_not_ready": "Cette demande d’ajout n’est pas prête à être fusionnée, vérifiez les évaluations et les signaux.", "repo.pulls.no_merge_access": "Vous n'êtes pas autorisé⋅e à fusionner cette demande d'ajout.", "repo.pulls.merge_pull_request": "Créer une révision de fusion", "repo.pulls.rebase_merge_pull_request": "Rebaser puis rattraper", @@ -1867,19 +1868,19 @@ "repo.pulls.push_rejected_summary": "Message de rejet complet", "repo.pulls.push_rejected_no_message": "Échec de la fusion : la soumission a été rejetée sans raison. Contrôler les déclencheurs Git pour ce dépôt.", "repo.pulls.open_unmerged_pull_exists": "Vous ne pouvez pas rouvrir ceci car la demande d’ajout #%d, en attente, a des propriétés identiques.", - "repo.pulls.status_checking": "Certains contrôles sont en attente", - "repo.pulls.status_checks_success": "Tous les contrôles ont réussi", - "repo.pulls.status_checks_warning": "Quelques vérifications ont signalé des avertissements", - "repo.pulls.status_checks_failure_required": "Des vérifications obligatoires ont échoué", - "repo.pulls.status_checks_failure_optional": "Des vérifications optionnelles ont échoué", - "repo.pulls.status_checks_error": "Quelques vérifications ont signalé des erreurs", + "repo.pulls.status_checking": "Certains signaux sont en attente", + "repo.pulls.status_checks_success": "Tous les signaux ont réussi", + "repo.pulls.status_checks_warning": "Des signaux déclarent des avertissements", + "repo.pulls.status_checks_failure_required": "Des signaux requis ont échoués", + "repo.pulls.status_checks_failure_optional": "Des signaux optionnels ont échoués", + "repo.pulls.status_checks_error": "Des signaux rapportent des erreurs", "repo.pulls.status_checks_requested": "Requis", "repo.pulls.status_checks_details": "Détails", - "repo.pulls.status_checks_hide_all": "Masquer toutes les vérifications", + "repo.pulls.status_checks_hide_all": "Masquer les signaux", "repo.pulls.status_checks_show_all": "Afficher toutes les vérifications", - "repo.pulls.status_checks_approve_all": "Accepter tous les flux de travail", - "repo.pulls.status_checks_need_approvals": "%d flux de travail en attente d’approbation", - "repo.pulls.status_checks_need_approvals_helper": "Ce flux de travail ne s’exécutera qu’après l’approbation par le mainteneur du dépôt.", + "repo.pulls.status_checks_approve_all": "Approuver toutes les procédures", + "repo.pulls.status_checks_need_approvals": "%d procédure(s) en attente d’approbation", + "repo.pulls.status_checks_need_approvals_helper": "Cette procédure ne s’executera qu’après l’approbation par le mainteneur du dépôt.", "repo.pulls.update_branch": "Actualiser la branche par fusion", "repo.pulls.update_branch_rebase": "Actualiser la branche par rebasage", "repo.pulls.update_branch_success": "La mise à jour de la branche a réussi", @@ -1963,8 +1964,8 @@ "repo.ext_wiki.desc": "Lier un wiki externe.", "repo.wiki": "Wiki", "repo.wiki.welcome": "Bienvenue sur le Wiki.", - "repo.wiki.welcome_desc": "Le wiki vous permet d'écrire ou de partager de la documentation avec vos collaborateurs.", - "repo.wiki.desc": "Écrire et partager de la documentation avec vos collaborateurs.", + "repo.wiki.welcome_desc": "Le wiki vous permet de rédiger et partager de la documentation avec des collaborateurs.", + "repo.wiki.desc": "Rédiger et partager de la documentation avec des collaborateurs.", "repo.wiki.create_first_page": "Créer la première page", "repo.wiki.page": "Page", "repo.wiki.filter_page": "Filtrer la page", @@ -1986,7 +1987,7 @@ "repo.wiki.pages": "Pages", "repo.wiki.last_updated": "Dernière mise à jour: %s", "repo.wiki.page_name_desc": "Entrez un nom pour cette page Wiki. Certains noms spéciaux sont « Home », « _Sidebar » et « _Footer ».", - "repo.wiki.original_git_entry_tooltip": "Voir le fichier Git original au lieu d'utiliser un lien convivial.", + "repo.wiki.original_git_entry_tooltip": "Voir le fichier Git original au lieu d’utiliser un lien convivial.", "repo.activity": "Activité", "repo.activity.navbar.pulse": "Impulsion", "repo.activity.navbar.code_frequency": "Fréquence du code", @@ -2066,23 +2067,23 @@ "repo.settings.public_access": "Accès public", "repo.settings.public_access_desc": "Configurer les permissions des visiteurs publics remplaçant les valeurs par défaut de ce dépôt.", "repo.settings.public_access.docs.not_set": "Non défini : ne donne aucune permission supplémentaire. Les règles du dépôt et les permissions des utilisateurs font foi.", - "repo.settings.public_access.docs.anonymous_read": "Lecture anonyme : les utilisateurs qui ne sont pas connectés peuvent consulter la ressource.", - "repo.settings.public_access.docs.everyone_read": "Consultation collective : tous les utilisateurs connectés peuvent consulter la ressource. Mettre les tickets et demandes d’ajouts en accès public signifie que les utilisateurs connectés peuvent en créer.", - "repo.settings.public_access.docs.everyone_write": "Participation collective : tous les utilisateurs connectés ont la permission d’écrire sur la ressource. Seule le Wiki supporte cette autorisation.", + "repo.settings.public_access.docs.anonymous_read": "Accès anonyme : les visiteurs non connectés peuvent consulter cette section.", + "repo.settings.public_access.docs.everyone_read": "Consultation collective : tous les utilisateurs connectés peuvent consulter cette section. Pour les Tickets et les Demandes d’ajouts, cela permet aussi aux utilisateurs connectés d’en créer.", + "repo.settings.public_access.docs.everyone_write": "Participation collective : tous les utilisateurs connectés peuvent participer à la section. Seul le Wiki supporte cette permission.", "repo.settings.collaboration": "Collaborateurs", "repo.settings.collaboration.admin": "Administrateur", "repo.settings.collaboration.write": "Écriture", "repo.settings.collaboration.read": "Lecture", "repo.settings.collaboration.owner": "Propriétaire", "repo.settings.collaboration.undefined": "Indéfini", - "repo.settings.collaboration.per_unit": "Permissions de ressource", + "repo.settings.collaboration.per_unit": "Permissions de section", "repo.settings.hooks": "Déclencheurs web", "repo.settings.githooks": "Déclencheurs Git", "repo.settings.basic_settings": "Paramètres de base", "repo.settings.mirror_settings": "Réglages Miroir", "repo.settings.mirror_settings.docs": "Configurez votre dépôt pour synchroniser automatiquement les révisions, étiquettes et branches avec un autre dépôt.", - "repo.settings.mirror_settings.docs.disabled_pull_mirror.instructions": "Configurez votre projet pour soumettre automatiquement les révisions, étiquettes et branches vers un autre dépôt. Les miroirs ont été désactivés par l'administrateur de votre site.", - "repo.settings.mirror_settings.docs.disabled_push_mirror.instructions": "Configurez votre projet pour synchroniser automatiquement les révisions, étiquettes et branches d'un autre dépôt.", + "repo.settings.mirror_settings.docs.disabled_pull_mirror.instructions": "Configurez votre projet pour soumettre automatiquement les révisions, étiquettes et branches vers un autre dépôt. Les miroirs ont été désactivés par l’administrateur de votre site.", + "repo.settings.mirror_settings.docs.disabled_push_mirror.instructions": "Configurez votre projet pour synchroniser automatiquement les révisions, étiquettes et branches d’un autre dépôt.", "repo.settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning": "Pour l’instant, cela ne peut être fait que dans le menu « Nouvelle migration ». Pour plus d’informations, veuillez consulter :", "repo.settings.mirror_settings.docs.disabled_push_mirror.info": "Les miroirs push ont été désactivés par l’administrateur de votre site.", "repo.settings.mirror_settings.docs.no_new_mirrors": "Votre dépôt se synchronise avec un dépôt distant. Vous ne pouvez pas créer de nouveaux miroirs pour le moment.", @@ -2201,13 +2202,13 @@ "repo.settings.trust_model.default.desc": "Utiliser le niveau de confiance configuré par défaut pour cette instance Gitea.", "repo.settings.trust_model.collaborator": "Collaborateur", "repo.settings.trust_model.collaborator.long": "Collaborateur : ne se fier qu'aux signatures des collaborateurs du dépôt", - "repo.settings.trust_model.collaborator.desc": "La signature d’une révision est dite « fiable » si elle correspond à un collaborateur du dépôt, indépendamment de son auteur. À défaut, si elle correspond à l’auteur de la révision, elle sera « dilettante », et « discordante » sinon.", + "repo.settings.trust_model.collaborator.desc": "Une révision est réputée authentifiée si elle est signée par un collaborateur du dépôt. Si elle n‘est que signée par son auteur, elle sera réputée dilettante, et discordante sinon.", "repo.settings.trust_model.committer": "Auteur", "repo.settings.trust_model.committer.long": "Auteur : ne se fier qu’aux signatures des auteurs des révisions (mimique GitHub en forçant Gitea à co-signer ses révisions).", - "repo.settings.trust_model.committer.desc": "La signature d’une révision est dite « fiable » si elle corresponds à son auteur, autrement elle est « discordante ». Pour les révisions déléguées à Gitea, elles seront signées par Gitea et l’auteur original sera crédité \"Co-authored-by:\" et \"Co-committed-by:\" en pied de révision. Pour cela, la clé configurée par défaut de Gitea doit correspondre à celle d’un utilisateur.", + "repo.settings.trust_model.committer.desc": "Une révision est réputée authentifiée si elle est signée par son auteur, et discordante si les signatures diffèrent. Cela force Gitea à signer ses propres révisions en créditant l’auteur original en pied de révision \"Co-authored-by:\" et \"Co-committed-by:\". La clé par défaut de Gitea doit correspondre à celle d’un utilisateur existant.", "repo.settings.trust_model.collaboratorcommitter": "Collaborateur et Auteur", "repo.settings.trust_model.collaboratorcommitter.long": "Collaborateur et Auteur : ne se fier qu'aux signatures des auteurs collaborant au dépôt", - "repo.settings.trust_model.collaboratorcommitter.desc": "La signature d’une révision est dite « fiable » si elle correponds à l’auteur collaborant au dépôt. Elle est « dilettante » si elle ne correponds qu’à l'auteur, et autrement « discordante ». Pour les révisions déléguées à Gitea, elles seront signées par Gitea et l’auteur original sera crédité \"Co-authored-by:\" et \"Co-committed-by:\" en pied de révision. Pour cela, la clé configurée par défaut de Gitea doit correspondre à celle d’un utilisateur.", + "repo.settings.trust_model.collaboratorcommitter.desc": "Une révision est réputée authentifiée si est elle signée par son auteur étant lui-même collaborateur du dépôt. Si elle n‘est que signée par son auteur, elle sera réputée dilettante, et discordante sinon. Cela force Gitea à signer ses propres révisions en créditant l’auteur original en pied de révision \"Co-authored-by:\". La clé par défaut de Gitea doit correspondre à celle d’un utilisateur existant.", "repo.settings.wiki_delete": "Supprimer les données du Wiki", "repo.settings.wiki_delete_desc": "Supprimer les données du wiki d'un dépôt est permanent. Cette action est irréversible.", "repo.settings.wiki_delete_notices_1": "- Ceci supprimera de manière permanente et désactivera le wiki de dépôt pour %s.", @@ -2220,7 +2221,7 @@ "repo.settings.delete_notices_fork_1": "- Les bifurcations de ce dépôt deviendront indépendants après suppression.", "repo.settings.deletion_success": "Le dépôt a été supprimé.", "repo.settings.update_settings_success": "Les options du dépôt ont été mises à jour.", - "repo.settings.update_settings_no_unit": "Impossible de désactiver toutes les fonctionnalités d'un dépôt. Vous ne pourrez gère l'utiliser.", + "repo.settings.update_settings_no_unit": "Si vous désactivez toutes les sections du dépôt, vous ne pourrez gère plus l’utiliser.", "repo.settings.confirm_delete": "Supprimer le dépôt", "repo.settings.add_collaborator": "Ajouter un collaborateur", "repo.settings.add_collaborator_success": "Le collaborateur a été ajouté.", @@ -2250,7 +2251,6 @@ "repo.settings.webhook_deletion_success": "Le webhook a été supprimé.", "repo.settings.webhook.test_delivery": "Tester l’envoi", "repo.settings.webhook.test_delivery_desc": "Testez ce webhook avec un faux événement.", - "repo.settings.webhook.test_delivery_desc_disabled": "Pour tester ce webhook avec un faux événement, activez-le.", "repo.settings.webhook.request": "Requête", "repo.settings.webhook.response": "Réponse", "repo.settings.webhook.headers": "Entêtes", @@ -2294,7 +2294,7 @@ "repo.settings.event_release": "Publication", "repo.settings.event_release_desc": "Publication publiée, mise à jour ou supprimée.", "repo.settings.event_push": "Soumission", - "repo.settings.event_force_push": "Poussée forcée", + "repo.settings.event_force_push": "Soumission forcée", "repo.settings.event_push_desc": "Soumission Git.", "repo.settings.event_repository": "Dépôt", "repo.settings.event_repository_desc": "Dépôt créé ou supprimé.", @@ -2328,11 +2328,11 @@ "repo.settings.event_pull_request_review_request_desc": "Création ou suppresion de demandes d’évaluation.", "repo.settings.event_pull_request_approvals": "Approbations de demande d'ajout", "repo.settings.event_pull_request_merge": "Fusion de demande d'ajout", - "repo.settings.event_header_workflow": "Événements du flux de travail", - "repo.settings.event_workflow_run": "Exécution du flux de travail", - "repo.settings.event_workflow_run_desc": "Tâche du flux de travail Gitea Actions ajoutée, en attente, en cours ou terminée.", - "repo.settings.event_workflow_job": "Tâches du flux de travail", - "repo.settings.event_workflow_job_desc": "Tâches du flux de travail Gitea Actions en file d’attente, en attente, en cours ou terminée.", + "repo.settings.event_header_workflow": "Événements de procédure", + "repo.settings.event_workflow_run": "Exécution de procédure", + "repo.settings.event_workflow_run_desc": "Exécution des procédures des Actions Gitea ajoutée, en attente, en cours ou terminées.", + "repo.settings.event_workflow_job": "Missions de la procédure", + "repo.settings.event_workflow_job_desc": "Les missions ajoutées, en attente, en cours ou terminées des Actions Gitea.", "repo.settings.event_package": "Paquet", "repo.settings.event_package_desc": "Paquet créé ou supprimé.", "repo.settings.branch_filter": "Filtre de branche", @@ -2392,44 +2392,44 @@ "repo.settings.protected_branch_can_push_no": "Vous ne pouvez pas soumettre", "repo.settings.branch_protection": "Paramètres de protection de branches pour la branche %s", "repo.settings.protect_this_branch": "Activer la protection de branche", - "repo.settings.protect_this_branch_desc": "Empêche les suppressions et limite les poussées et fusions sur cette branche.", + "repo.settings.protect_this_branch_desc": "Empêche la suppression et Git de soumettre et fusionner sur cette branche.", "repo.settings.protect_disable_push": "Désactiver la soumission", "repo.settings.protect_disable_push_desc": "Aucune soumission ne sera possible sur cette branche.", - "repo.settings.protect_disable_force_push": "Désactiver les poussés forcées", - "repo.settings.protect_disable_force_push_desc": "Aucune poussée forcée ne sera possible sur cette branche.", + "repo.settings.protect_disable_force_push": "Désactiver la soumission forcée", + "repo.settings.protect_disable_force_push_desc": "Aucune soumission forcée ne sera possible sur cette branche.", "repo.settings.protect_enable_push": "Activer la soumission", "repo.settings.protect_enable_push_desc": "Toute personne ayant un accès en écriture sera autorisée à soumettre sur cette branche (sans forcer).", - "repo.settings.protect_enable_force_push_all": "Activer les poussées forcées", - "repo.settings.protect_enable_force_push_all_desc": "Toute personne pouvant pousser pourra forcer sur cette branche.", + "repo.settings.protect_enable_force_push_all": "Activer la soumission forcée", + "repo.settings.protect_enable_force_push_all_desc": "Toute personne pouvant soumettre pourra forcer sur cette branche.", "repo.settings.protect_enable_force_push_allowlist": "Soumission forcée sur autorisation uniquement", - "repo.settings.protect_enable_force_push_allowlist_desc": "Seuls les utilisateurs ou équipes autorisés ayants un droit de pousser seront autorisés à pousser en force sur cette branche.", + "repo.settings.protect_enable_force_push_allowlist_desc": "Seuls les utilisateurs ou équipes autorisés ayants un droit de soumission seront autorisés à soumettre en force sur cette branche.", "repo.settings.protect_enable_merge": "Activer la fusion", "repo.settings.protect_enable_merge_desc": "Toute personne ayant un accès en écriture sera autorisée à fusionner les demandes d'ajout dans cette branche.", "repo.settings.protect_whitelist_committers": "Soumissions sur autorisation uniquement", - "repo.settings.protect_whitelist_committers_desc": "Seuls les utilisateurs ou les équipes autorisés pourront pousser sur cette branche (sans forcer).", + "repo.settings.protect_whitelist_committers_desc": "Seuls les utilisateurs ou les équipes autorisés pourront soumettre sur cette branche (sans forcer).", "repo.settings.protect_whitelist_deploy_keys": "Clés de déploiement pouvant écrire autorisées à pousser.", "repo.settings.protect_whitelist_users": "Utilisateurs autorisés à pousser :", "repo.settings.protect_whitelist_teams": "Équipes autorisées à pousser :", - "repo.settings.protect_force_push_allowlist_users": "Utilisateurs autorisés à pousser en force :", - "repo.settings.protect_force_push_allowlist_teams": "Équipes autorisées à pousser en force :", - "repo.settings.protect_force_push_allowlist_deploy_keys": "Clés de déploiement pouvant pousser autorisées à pousser en force.", + "repo.settings.protect_force_push_allowlist_users": "Utilisateurs autorisés à soumettre en force :", + "repo.settings.protect_force_push_allowlist_teams": "Équipes autorisées à soumettre en force :", + "repo.settings.protect_force_push_allowlist_deploy_keys": "Inclure aussi les clés de déploiement autorisées à soumettre.", "repo.settings.protect_merge_whitelist_committers": "Fusion sur autorisation uniquement", "repo.settings.protect_merge_whitelist_committers_desc": "N’autoriser que les utilisateurs et les équipes listés à appliquer les demandes de fusion sur cette branche.", "repo.settings.protect_merge_whitelist_users": "Utilisateurs autorisés à fusionner :", "repo.settings.protect_merge_whitelist_teams": "Équipes autorisées à fusionner :", "repo.settings.protect_bypass_allowlist": "Contourner la protection de la branche", "repo.settings.protect_enable_bypass_allowlist": "Autoriser des utilisateurs et des équipes à contourner les restrictions de branche", - "repo.settings.protect_enable_bypass_allowlist_desc": "Les utilisateurs ou équipes autorisés peuvent fusionner ou pousser des changements nonobstant les règles d’approbations, de vérifications de statut et les protections fichiers.", + "repo.settings.protect_enable_bypass_allowlist_desc": "Les utilisateurs ou équipes autorisés peuvent fusionner ou soumettre des changements nonobstant les règles d’approbations, de vérifications des signaux et les protections de fichiers.", "repo.settings.protect_bypass_allowlist_users": "Liste d’utilisateurs autorisés à contourner les protections :", "repo.settings.protect_bypass_allowlist_teams": "Liste d’équipes autorisées à contourner les protections :", - "repo.settings.protect_check_status_contexts": "Activer le Contrôle Qualité", - "repo.settings.protect_status_check_patterns": "Motifs de vérification des statuts :", - "repo.settings.protect_status_check_patterns_desc": "Entrez des motifs pour spécifier quelles vérifications doivent réussir avant que des branches puissent être fusionnées. Un motif par ligne. Un motif ne peut être vide.", - "repo.settings.protect_check_status_contexts_desc": "Exiger le status « succès » avant de fusionner. Quand activée, une branche protégée ne peux accepter que des soumissions ou des fusions ayant le status « succès ». Lorsqu'il n’y a pas de contexte, la dernière révision fait foi.", - "repo.settings.protect_check_status_contexts_list": "Contrôles qualité trouvés au cours de la semaine dernière pour ce dépôt", + "repo.settings.protect_check_status_contexts": "Activer les signaux", + "repo.settings.protect_status_check_patterns": "Motifs de signal :", + "repo.settings.protect_status_check_patterns_desc": "Entrez des motifs pour spécifier quelles signaux doivent réussir avant que des branches puissent être fusionnées. Un motif par ligne. Un motif ne peut être vide.", + "repo.settings.protect_check_status_contexts_desc": "Exiger la réussite des signaux avant de fusionner. Quand activé, une branche protégée ne peux accepter que des soumissions ou des fusions ayant le status « succès ». Lorsqu'il n’y a pas de contexte, la dernière révision fait foi.", + "repo.settings.protect_check_status_contexts_list": "Signaux trouvés au cours de la semaine passée pour ce dépôt", "repo.settings.protect_status_check_matched": "Correspondant", - "repo.settings.protect_invalid_status_check_pattern": "Motif de vérification des statuts incorrect : « %s ».", - "repo.settings.protect_no_valid_status_check_patterns": "Aucun motif de vérification des statuts valide.", + "repo.settings.protect_invalid_status_check_pattern": "Motif de signal invalide : « %s ».", + "repo.settings.protect_no_valid_status_check_patterns": "Aucun motif de signaux valide.", "repo.settings.protect_required_approvals": "Minimum d'approbations requis :", "repo.settings.protect_required_approvals_desc": "Permet de fusionner les demandes d’ajout lorsque suffisamment d’évaluation sont positives.", "repo.settings.protect_approvals_whitelist_enabled": "Restreindre les approbations sur autorisation uniquement", @@ -2455,15 +2455,15 @@ "repo.settings.remove_protected_branch_success": "La règle de protection de branche \"%s\" a été retirée.", "repo.settings.remove_protected_branch_failed": "Impossible de retirer la règle de protection de branche \"%s\".", "repo.settings.protected_branch_deletion": "Désactiver la protection de branche", - "repo.settings.protected_branch_deletion_desc": "Désactiver la protection de branche permet aux utilisateurs ayant accès en écriture de pousser des modifications sur la branche. Continuer ?", + "repo.settings.protected_branch_deletion_desc": "Désactiver la protection de branche permet aux utilisateurs ayant accès en écriture de soumettre des modifications sur la branche. Continuer ?", "repo.settings.block_rejected_reviews": "Bloquer la fusion en cas d’évaluations négatives", "repo.settings.block_rejected_reviews_desc": "La fusion ne sera pas possible lorsque des modifications sont demandées par les évaluateurs officiels, même s'il y a suffisamment d’approbations.", "repo.settings.block_on_official_review_requests": "Bloquer la fusion en cas de demande d’évaluation officielle", "repo.settings.block_on_official_review_requests_desc": "La fusion ne sera pas possible tant qu’elle aura des demandes d’évaluations officielles, même s'il y a suffisamment d’approbations.", "repo.settings.block_outdated_branch": "Bloquer la fusion si la demande d'ajout est obsolète", "repo.settings.block_outdated_branch_desc": "La fusion ne sera pas possible lorsque la branche principale est derrière la branche de base.", - "repo.settings.block_admin_merge_override": "Les administrateurs doivent respecter les règles de protection des branches", - "repo.settings.block_admin_merge_override_desc": "Les administrateurs sont également soumis aux règles de protection des branches. En activant la liste d’autorisation de contournement, les utilisateurs et équipes qui y figurent peuvent toujours contourner ces règles.", + "repo.settings.block_admin_merge_override": "Inclure les administrateurs dans l’application de la règle", + "repo.settings.block_admin_merge_override_desc": "Les administrateurs sont également soumis aux règles de protection des branches. En revanche, les utilisateurs et équipes qui figurent sur la liste dérogatoire y sont exemptés.", "repo.settings.default_branch_desc": "Sélectionnez une branche par défaut pour les révisions.", "repo.settings.default_target_branch_desc": "Les demandes d’ajout peuvent utiliser une branche cible différente, telle que définie dans la section Demandes d’ajouts des Paramètres avancés du dépôt.", "repo.settings.merge_style_desc": "Styles de fusion", @@ -2598,6 +2598,9 @@ "repo.diff.review.reject": "Demander des changements", "repo.diff.review.self_approve": "Les auteurs d’une demande d’ajout ne peuvent pas approuver leur propre demande d’ajout", "repo.diff.committed_by": "révisé par", + "repo.diff.coauthored_by": "coécrit par", + "repo.commits.avatar_stack_and": "et", + "repo.commits.avatar_stack_people": "%d personne(s)", "repo.diff.protected": "Protégé", "repo.diff.image.side_by_side": "Côte à côte", "repo.diff.image.swipe": "Glisser", @@ -2717,7 +2720,7 @@ "repo.error.csv.too_large": "Impossible de visualiser le fichier car il est trop volumineux.", "repo.error.csv.unexpected": "Impossible de visualiser ce fichier car il contient un caractère inattendu ligne %d, colonne %d.", "repo.error.csv.invalid_field_count": "Impossible de visualiser ce fichier car il contient un nombre de champs incorrect à la ligne %d.", - "repo.error.broken_git_hook": "Les crochets Git de ce dépôt semblent cassés. Veuillez suivre la documentation pour les corriger, puis pousser des révisions pour actualiser le statut.", + "repo.error.broken_git_hook": "Les crochets Git de ce dépôt semblent cassés. Veuillez suivre la documentation pour les corriger, puis soummettre des révisions pour actualiser le statut.", "graphs.component_loading": "Chargement de %s…", "graphs.component_loading_failed": "Impossible de charger %s.", "graphs.component_loading_info": "Ça prend son temps…", @@ -2725,6 +2728,7 @@ "graphs.code_frequency.what": "fréquence du code", "graphs.contributors.what": "contributions", "graphs.recent_commits.what": "révisions récentes", + "graphs.chart_zoom_hint": "Glisser : zoom, Maj + Glisser : pano, Double-clic : recentrer", "org.org_name_holder": "Nom de l'organisation", "org.org_full_name_holder": "Nom complet de l'organisation", "org.org_name_helper": "Le nom de l'organisation doit être court et mémorable.", @@ -2746,8 +2750,8 @@ "org.team_desc_helper": "Décrire le but ou le rôle de l’équipe.", "org.team_access_desc": "Accès au dépôt", "org.team_permission_desc": "Autorisation", - "org.team_unit_desc": "Permettre l’accès aux Sections du dépôt", - "org.team_unit_disabled": "(Désactivé)", + "org.team_unit_desc": "Permettre l’accès aux sections du dépôt", + "org.team_unit_disabled": "(Désactivée)", "org.form.name_been_taken": "Le nom d’organisation « %s » a déjà été utilisé.", "org.form.name_reserved": "Le nom d'organisation \"%s\" est réservé.", "org.form.name_pattern_not_allowed": "Le motif « %s » n'est pas autorisé dans un nom d'organisation.", @@ -2815,15 +2819,15 @@ "org.teams.can_create_org_repo": "Créer des dépôts", "org.teams.can_create_org_repo_helper": "Les membres peuvent créer de nouveaux dépôts dans l'organisation. Le créateur obtiendra l'accès administrateur au nouveau dépôt.", "org.teams.none_access": "Aucun accès", - "org.teams.none_access_helper": "Les membres ne peuvent voir ou faire quoi que ce soit sur cette partie. Sans effet pour les dépôts publics.", + "org.teams.none_access_helper": "Les membres ne peuvent ni consulter ni participer à cette section. Ne s’applique pas pour les dépôts publics.", "org.teams.general_access": "Accès général", "org.teams.general_access_helper": "Les permissions des membres seront déterminées par la table des permissions ci-dessous.", "org.teams.read_access": "Lecture", - "org.teams.read_access_helper": "Les membres peuvent voir et cloner les dépôts de l'équipe.", + "org.teams.read_access_helper": "Les membres peuvent voir et cloner les dépôts de l’équipe.", "org.teams.write_access": "Écriture", - "org.teams.write_access_helper": "Les membres peuvent voir et pousser dans les dépôts de l'équipe.", + "org.teams.write_access_helper": "Les membres peuvent consulter et soumettre sur les dépôts de l’équipe.", "org.teams.admin_access": "Accès Administrateur", - "org.teams.admin_access_helper": "Les membres peuvent tirer et pousser des modifications vers les dépôts de l'équipe, et y ajouter des collaborateurs.", + "org.teams.admin_access_helper": "Les membres peuvent extraire et soumettre les dépôts de l’équipe et y ajouter des collaborateurs.", "org.teams.no_desc": "Aucune description", "org.teams.settings": "Paramètres", "org.teams.owners_permission_desc": "Les propriétaires ont un accès complet à tous les dépôts et disposent d'un accès administrateur de l'organisation.", @@ -2840,8 +2844,8 @@ "org.teams.delete_team_desc": "Supprimer une équipe supprime l'accès aux dépôts à ses membres. Continuer ?", "org.teams.delete_team_success": "L’équipe a été supprimée.", "org.teams.read_permission_desc": "Cette équipe permet l'accès en lecture : les membres peuvent voir et dupliquer ses dépôts.", - "org.teams.write_permission_desc": "Cette équipe permet l'accès en écriture : les membres peuvent participer à ses dépôts.", - "org.teams.admin_permission_desc": "Cette équipe permet l'accès administrateur : les membres peuvent voir, participer et ajouter des collaborateurs à ses dépôts.", + "org.teams.write_permission_desc": "Cette équipe accorde un accès en écriture : les membres peuvent participer à ses dépôts.", + "org.teams.admin_permission_desc": "Cette équipe accorde un accès administrateur : les membres peuvent consulter, participer et ajouter des collaborateurs à ses dépôts.", "org.teams.create_repo_permission_desc": "De plus, cette équipe accorde la permission Créer un dépôt : les membres peuvent créer de nouveaux dépôts dans l'organisation.", "org.teams.repositories": "Dépôts de l'Équipe", "org.teams.remove_all_repos_title": "Supprimer tous les dépôts de l'équipe", @@ -2858,8 +2862,16 @@ "org.teams.all_repositories": "Tous les dépôts", "org.teams.all_repositories_helper": "L'équipe a accès à tous les dépôts. Sélectionner ceci ajoutera tous les dépôts existants à l'équipe.", "org.teams.all_repositories_read_permission_desc": "Cette équipe accorde l'accès en lecture à tous les dépôts : les membres peuvent voir et cloner les dépôts.", - "org.teams.all_repositories_write_permission_desc": "Cette équipe accorde l'accès en écriture à tous les dépôts : les membres peuvent lire et écrire dans les dépôts.", - "org.teams.all_repositories_admin_permission_desc": "Cette équipe accorde l'accès administrateur à tous les dépôts : les membres peuvent lire, écrire dans et ajouter des collaborateurs aux dépôts.", + "org.teams.all_repositories_write_permission_desc": "Cette équipe accorde un accès en écriture à tous les dépôts : les membres peuvent participer aux dépôts.", + "org.teams.all_repositories_admin_permission_desc": "Cette équipe accorde un accès administrateur à tous les dépôts : les membres peuvent consulter, participer et ajouter des collaborateurs aux dépôts.", + "org.teams.visibility": "Visibilité", + "org.teams.visibility_private": "Privé", + "org.teams.visibility_private_helper": "Visible uniquement aux membres de l’équipe et aux propriétaires de l’organisation.", + "org.teams.visibility_limited": "Limité", + "org.teams.visibility_limited_helper": "Visible à tous les membres de cette organisation.", + "org.teams.visibility_public": "Publique", + "org.teams.visibility_public_helper": "Visible à tout utilisateur connecté.", + "org.teams.owners_visibility_fixed": "La visibilité des propriétaires ne peut pas être modifiée.", "org.teams.invite.title": "Vous avez été invité à rejoindre l'équipe %s dans l'organisation %s.", "org.teams.invite.by": "Invité par %s", "org.teams.invite.description": "Veuillez cliquer sur le bouton ci-dessous pour rejoindre l’équipe.", @@ -3001,7 +3013,7 @@ "admin.dashboard.gc_lfs": "Purger les métaobjets LFS", "admin.dashboard.stop_zombie_tasks": "Arrêter les tâches zombies", "admin.dashboard.stop_endless_tasks": "Arrêter les tâches interminables", - "admin.dashboard.cancel_abandoned_jobs": "Annuler les actions des tâches abandonnés", + "admin.dashboard.cancel_abandoned_jobs": "Annuler les actions des missions abandonnées", "admin.dashboard.start_schedule_tasks": "Démarrer les tâches planifiées", "admin.dashboard.sync_branch.started": "Début de la synchronisation des branches", "admin.dashboard.sync_tag.started": "Synchronisation des étiquettes", @@ -3293,7 +3305,6 @@ "admin.config.default_enable_timetracking": "Activer le suivi de temps par défaut", "admin.config.default_allow_only_contributors_to_track_time": "Restreindre le suivi de temps aux contributeurs", "admin.config.no_reply_address": "Domaine pour les courriels cachés", - "admin.config.default_visibility_organization": "Visibilité par défaut des nouvelles organisations", "admin.config.default_enable_dependencies": "Activer les dépendances pour les tickets par défaut", "admin.config.webhook_config": "Configuration Webhook", "admin.config.queue_length": "Longueur de la file d'attente", @@ -3438,7 +3449,7 @@ "action.merge_pull_request": "a fusionné la demande d’ajout %[3]s#%[2]s", "action.auto_merge_pull_request": "a fusionné automatiquement la demande d’ajout %[3]s#%[2]s", "action.transfer_repo": "a transféré le dépôt %s vers %s", - "action.push_tag": "a poussé l’étiquette %[3]s de %[4]s", + "action.push_tag": "a soumis l’étiquette %[3]s de %[4]s", "action.delete_tag": "a supprimé l’étiquette %[2]s de %[3]s", "action.delete_branch": "a supprimé la branche %[2]s de %[3]s", "action.compare_branch": "Comparer", @@ -3498,9 +3509,9 @@ "gpg.error.failed_retrieval_gpg_keys": "Impossible de récupérer la clé liée au compte de l'auteur", "gpg.error.probable_bad_signature": "AVERTISSEMENT ! Bien qu’il y ait une clé avec cet ID dans la base de données, elle ne vérifie pas cette révision ! Cette révision est SUSPECTE.", "gpg.error.probable_bad_default_signature": "AVERTISSEMENT ! Bien que la clé par défaut ait cet ID, elle ne vérifie pas cette révision ! Cette révision est SUSPECTE.", - "units.unit": "Ressource", - "units.error.no_unit_allowed_repo": "Vous n'êtes pas autorisé à accéder à n'importe quelle section de ce dépôt.", - "units.error.unit_not_allowed": "Vous n'êtes pas autorisé à accéder à cette section du dépôt.", + "units.unit": "Section", + "units.error.no_unit_allowed_repo": "Vous n’êtes pas autorisé à accéder à quelconque section de ce dépôt.", + "units.error.unit_not_allowed": "Vous n’êtes pas autorisé à accéder à cette section du dépôt.", "packages.title": "Paquets", "packages.desc": "Gérer les paquets du dépôt.", "packages.empty": "Il n'y pas de paquet pour le moment.", @@ -3714,64 +3725,66 @@ "actions.status.cancelling": "Annulation", "actions.status.skipped": "Ignoré", "actions.status.blocked": "Bloqué", - "actions.runners": "Exécuteurs", - "actions.runners.runner_manage_panel": "Gestion des exécuteurs", - "actions.runners.new": "Créer un nouvel exécuteur", - "actions.runners.new_notice": "Comment démarrer un exécuteur", + "actions.runners": "Opérateurs", + "actions.runners.runner_manage_panel": "Gestion des opérateurs", + "actions.runners.new": "Créer un nouvel opérateur", + "actions.runners.new_notice": "Comment démarrer un opérateur", "actions.runners.status": "Statut", "actions.runners.id": "ID", "actions.runners.name": "Nom", "actions.runners.owner_type": "Type", - "actions.runners.availability": "Disponibilité", + "actions.runners.availability": "Activation", "actions.runners.description": "Description", - "actions.runners.labels": "Labels", + "actions.runners.labels": "Libellés", "actions.runners.last_online": "Dernière fois en ligne", - "actions.runners.runner_title": "Exécuteur", - "actions.runners.task_list": "Tâches récentes sur cet exécuteur", + "actions.runners.runner_title": "Opérateur", + "actions.runners.task_list": "Tâches récentes de cet opérateur", "actions.runners.task_list.no_tasks": "Il n'y a pas de tâche ici.", - "actions.runners.task_list.run": "Exécuter", + "actions.runners.task_list.run": "Exécution", "actions.runners.task_list.status": "Statut", "actions.runners.task_list.repository": "Dépôt", "actions.runners.task_list.commit": "Révision", "actions.runners.task_list.done_at": "Fait à", - "actions.runners.edit_runner": "Éditer l'Exécuteur", + "actions.runners.edit_runner": "Éditer l’opérateur", "actions.runners.update_runner": "Appliquer les modifications", - "actions.runners.update_runner_success": "Exécuteur mis à jour avec succès", - "actions.runners.update_runner_failed": "Impossible d'actualiser l'Exécuteur", - "actions.runners.enable_runner": "Activer cet exécuteur", - "actions.runners.enable_runner_success": "Exécuteur activé avec succès", - "actions.runners.enable_runner_failed": "Impossible d’activer l’exécuteur", - "actions.runners.disable_runner": "Désactiver cet exécuteur", - "actions.runners.disable_runner_success": "Exécuteur désactivé avec succès", - "actions.runners.disable_runner_failed": "Impossible de désactiver l’exécuteur", - "actions.runners.delete_runner": "Supprimer cet exécuteur", - "actions.runners.delete_runner_success": "Exécuteur supprimé avec succès", - "actions.runners.delete_runner_failed": "Impossible de supprimer l'Exécuteur", - "actions.runners.delete_runner_header": "Êtes-vous sûr de vouloir supprimer cet exécuteur ?", - "actions.runners.delete_runner_notice": "Si une tâche est en cours sur cet exécuteur, elle sera terminée et marquée comme échouée. Cela risque d’interrompre le flux de travail.", - "actions.runners.none": "Aucun exécuteur disponible", + "actions.runners.update_runner_success": "Opérateur mis à jour avec succès", + "actions.runners.update_runner_failed": "Impossible d'actualiser l’opérateur", + "actions.runners.enable_runner": "Activer cet opérateur", + "actions.runners.enable_runner_success": "Opérateur activé avec succès", + "actions.runners.enable_runner_failed": "Impossible d’activer l’opérateur", + "actions.runners.disable_runner": "Désactiver cet opérateur", + "actions.runners.disable_runner_success": "Opérateur désactivé avec succès", + "actions.runners.disable_runner_failed": "Impossible de désactiver l’opérateur", + "actions.runners.delete_runner": "Supprimer cet opérateur", + "actions.runners.delete_runner_success": "Opérateur supprimé avec succès", + "actions.runners.delete_runner_failed": "Impossible de supprimer l’opérateur", + "actions.runners.delete_runner_header": "Êtes-vous sûr de vouloir supprimer cet opérateur ?", + "actions.runners.delete_runner_notice": "Si des tâches sont en cours de réalisation par cet opérateur, elles seront abandonnées et marquées en échec, pouvant également entrainer l’échec des procédures appelantes.", + "actions.runners.none": "Aucun opérateur disponible", "actions.runners.status.unspecified": "Inconnu", - "actions.runners.status.idle": "Inactif", - "actions.runners.status.active": "Actif", + "actions.runners.status.idle": "Disponible", + "actions.runners.status.active": "Occupé", "actions.runners.status.offline": "Hors-ligne", "actions.runners.version": "Version", - "actions.runners.reset_registration_token": "Réinitialiser le jeton d'enregistrement", + "actions.runners.reset_registration_token": "Réinitialiser le jeton d’enregistrement", "actions.runners.reset_registration_token_confirm": "Voulez-vous révoquer le jeton actuel et en générer un nouveau ?", - "actions.runners.reset_registration_token_success": "Le jeton d’inscription de l’exécuteur a été réinitialisé avec succès", - "actions.runs.all_workflows": "Tous les flux de travail", - "actions.runs.other_workflows": "Autres flux de travail", - "actions.runs.other_workflows_tooltip": "Les flux de travail qui ont été exécutés dans ce dépôt mais qui n’existent pas dans la branche par défaut.", - "actions.runs.workflow_run_count_1": "%d exécution du workflow", - "actions.runs.workflow_run_count_n": "%d exécutions du workflow", + "actions.runners.reset_registration_token_success": "Le jeton d’inscription de l’opérateur a été réinitialisé avec succès", + "actions.runs.all_workflows": "Toutes les procédures", + "actions.runs.other_workflows": "Autres procédures", + "actions.runs.other_workflows_tooltip": "Les procédures qui ont été exécutées dans ce dépôt mais qui n’existent pas dans la branche par défaut.", + "actions.runs.workflow_run_count_1": "%d exécution de procédure", + "actions.runs.workflow_run_count_n": "%d exécutions de procédure", "actions.runs.commit": "Révision", "actions.runs.run_details": "Détails de l’exécution", - "actions.runs.workflow_file": "Fichier de flux de travail", + "actions.runs.workflow_file": "Déclaration de la procédure", + "actions.runs.workflow_file_no_permission": "Pas de permission pour voir la procédure", "actions.runs.scheduled": "Planifié", "actions.runs.pushed_by": "soumis par", - "actions.runs.invalid_workflow_helper": "La configuration du flux de travail est invalide. Veuillez vérifier votre fichier %s.", - "actions.runs.no_matching_online_runner_helper": "Aucun exécuteur en ligne correspondant au libellé %s", - "actions.runs.no_job_without_needs": "Le flux de travail doit contenir au moins une tâche sans dépendance.", - "actions.runs.no_job": "Le flux de travail doit contenir au moins une tâche", + "actions.runs.invalid_workflow_helper": "La déclaration de la procédure est invalide. Veuillez vérifier le fichier « %s ».", + "actions.runs.no_matching_online_runner_helper": "Aucun opérateur disponible correspondant au libellé « %s »", + "actions.runs.no_job_without_needs": "La procédure doit contenir au moins une mission sans dépendance.", + "actions.runs.no_job": "La procédure doit contenir au moins une mission.", + "actions.runs.invalid_reusable_workflow_uses": "Clause \"uses\" invalide dans la procédure : %s", "actions.runs.actor": "Acteur", "actions.runs.status": "Statut", "actions.runs.actors_no_select": "Tous les acteurs", @@ -3779,37 +3792,82 @@ "actions.runs.branch": "Branche", "actions.runs.branches_no_select": "Toutes les branches", "actions.runs.no_results": "Aucun résultat correspondant.", - "actions.runs.no_workflows": "Il n'y a pas encore de workflows.", - "actions.runs.no_workflows.quick_start": "Vous découvrez les Actions Gitea ? Consultez le didacticiel.", - "actions.runs.no_workflows.documentation": "Pour plus d’informations sur les actions Gitea, voir la documentation.", - "actions.runs.no_runs": "Le flux de travail n'a pas encore d'exécution.", + "actions.runs.no_workflows": "Il n’y a pas de procédure ici.", + "actions.runs.no_workflows.quick_start": "Vous découvrez les Actions Gitea ? Consultez le didacticiel.", + "actions.runs.no_workflows.documentation": "Pour plus d’informations sur les Actions Gitea, voir la documentation.", + "actions.runs.no_runs": "Cette procédure n’a pas encore été exécutée.", "actions.runs.empty_commit_message": "(message de révision vide)", "actions.runs.expire_log_message": "Les journaux ont été supprimés car ils étaient trop anciens.", - "actions.runs.delete": "Supprimer cette exécution", - "actions.runs.cancel": "Annuler l’exécution du flux", + "actions.runs.delete": "Effacer l’execution de cette procédure", + "actions.runs.cancel": "Annuler l’exécution de cette procédure", "actions.runs.delete.description": "Êtes-vous sûr de vouloir supprimer définitivement cette exécution ? Cette action ne peut pas être annulée.", - "actions.runs.not_done": "Cette exécution du flux de travail n’est pas terminée.", - "actions.runs.view_workflow_file": "Voir le fichier du flux de travail", + "actions.runs.not_done": "Cette procédure n’est pas terminée.", + "actions.runs.view_workflow_file": "Voir la déclaration de la procédure", "actions.runs.summary": "Résumé", - "actions.runs.all_jobs": "Toutes les tâches", + "actions.runs.all_jobs": "Toutes les missions", + "actions.runs.job_summaries": "Résumé des missions", + "actions.runs.expand_caller_jobs": "Afficher les missions de cette procédure réutilisable", + "actions.runs.collapse_caller_jobs": "Masquer les missions de cette procédure réutilisable", "actions.runs.attempt": "Tentative", "actions.runs.latest": "Dernière", "actions.runs.latest_attempt": "Dernière tentative", "actions.runs.triggered_via": "Déclenché via %s", + "actions.runs.rerun_triggered": "Relance enclenchée", + "actions.runs.back_to_pull_request": "Retour à la demande d’ajout", + "actions.runs.back_to_workflow": "Retour à la procédure", "actions.runs.total_duration": "Durée totale :", - "actions.workflow.disable": "Désactiver le flux de travail", - "actions.workflow.disable_success": "Le flux de travail « %s » a bien été désactivé.", - "actions.workflow.enable": "Activer le flux de travail", - "actions.workflow.enable_success": "Le flux de travail « %s » a bien été activé.", - "actions.workflow.disabled": "Le flux de travail est désactivé.", - "actions.workflow.run": "Exécuter le flux de travail", - "actions.workflow.not_found": "Flux de travail « %s » introuvable.", - "actions.workflow.run_success": "Le flux de travail « %s » s’est correctement exécuté.", - "actions.workflow.from_ref": "Utiliser le flux de travail depuis", - "actions.workflow.has_workflow_dispatch": "Ce flux de travail a un déclencheur d’événement workflow_dispatch.", - "actions.workflow.has_no_workflow_dispatch": "Le flux de travail %s n’a pas de déclencheur d’événement workflow_dispatch.", - "actions.need_approval_desc": "Besoin d’approbation pour exécuter des flux de travail pour une demande d’ajout de bifurcation.", - "actions.approve_all_success": "Tous les flux de travail ont été acceptés.", + "actions.runs.workflow_dependencies": "Dépendances de la procédure", + "actions.runs.graph_jobs_count_1": "%d mission", + "actions.runs.graph_jobs_count_n": "%d missions", + "actions.runs.graph_dependencies_count_1": "%d dépendance", + "actions.runs.graph_dependencies_count_n": "%d dépendances", + "actions.runs.graph_success_rate": "%s succès", + "actions.runs.graph_zoom_in": "Zoomer (Ctrl/⌘ + défilement)", + "actions.runs.graph_zoom_max": "Déjà à 100%", + "actions.runs.graph_zoom_out": "Dézoomer (Ctrl/⌘ + défilement)", + "actions.runs.graph_reset_view": "Rétablir", + "actions.workflow.disable": "Désactiver la procédure", + "actions.workflow.disable_success": "La procédure « %s » a bien été désactivée.", + "actions.workflow.enable": "Activer la procédure", + "actions.workflow.enable_success": "La procédure « %s » a bien été activée.", + "actions.workflow.disabled": "La procédure est désactivée.", + "actions.workflow.scope_owner": "Propriétaire", + "actions.workflow.scope_global": "Global", + "actions.workflow.required": "Requis", + "actions.workflow.scoped_required_cannot_disable": "Cette procédure transversale est requise.", + "actions.scoped_workflows": "Procédures transversales", + "actions.scoped_workflows.desc_org": "Enrôlez un dépôt afin de rendre ses procédures accessibles à votre organisation. Toutes les procédures de la branche principale de ce dépôt seront ainsi exécutées dans chaque dépôt de cette organisation, comme si elles y avaient été créées.", + "actions.scoped_workflows.desc_user": "Enrôlez un dépôt afin de rendre ses procédures accessibles à votre compte. Toutes les procédures de la branche principale de ce dépôt seront ainsi exécutées dans chaque dépôt que vous possédez, comme si elles y avaient été créées.", + "actions.scoped_workflows.desc_global": "Enrôlez un dépôt afin de rendre ses procédures accessibles à l’ensemble du serveur. Toutes les procédures de la branche principale de ce dépôt seront ainsi exécutées dans chaque dépôt, comme si elles y avaient été créées. Sur un serveur volumineux, ces procédures peuvent lourdement solliciter les ressources du système.", + "actions.scoped_workflows.add_help": "Pour rendre des procédures transversales, soumettez leurs déclarations dans le dossier %s sur la branche par défaut de ce dépôt, puis enrôlez celui-ci ci-dessous.", + "actions.scoped_workflows.security_note": "Parce qu’une procédure transversale opère sur d’autres dépôts que le sien, ses extrants sont journalisés sur ces dépôts et sont donc consultable par leurs utilisateurs. Ainsi, une procédure issue d’un dépôt privé peut-être reconstruit à partir des journaux qu'elle produit. Exposer une procédure transversale peut donc compromètre la confidentialité de son dépôt hôte. Si une procédure transversale référence une autre procédure issue d’un dépôt privé, assurez-vous que les dépôts affectés puissent également s'y référer, sans quoi ces procédures échoueront.", + "actions.scoped_workflows.source.add": "Enrôler un dépôt", + "actions.scoped_workflows.source.add_success": "Dépôt enrôlé.", + "actions.scoped_workflows.source.remove_success": "Dépôt retiré.", + "actions.scoped_workflows.source.not_found": "Dépôt introuvable.", + "actions.scoped_workflows.required.update_success": "Procédure requise mise à jour.", + "actions.scoped_workflows.required.label": "Marquer les procédures comme requises (elles ne pourront être désactivés depuis un dépôt) :", + "actions.scoped_workflows.required.patterns": "Motifs de signal requis", + "actions.scoped_workflows.required.patterns_aria": "Motifs de signal requis pour « %s »", + "actions.scoped_workflows.required.patterns_note": "est uniquement appliqué lorsque la procédure est requise", + "actions.scoped_workflows.required.patterns_hint": "Marquez la procédure comme requise pour configurer ses motifs de signal.", + "actions.scoped_workflows.required.patterns_help": "Un motif de signal par ligne. Une demande d’ajout concernée peut être fusionnée à condition qu’au moins un signal par motif ait réussi. Cela est imposé pour toutes les branches affectées ayant des règles de protections (même désactivées), mais pas les branches sans protections.", + "actions.scoped_workflows.required.patterns_empty": "Chaque procédure requise nécessite au moins un motif de signal.", + "actions.scoped_workflows.required.missing_file": "Le fichier n’est plus dans le dépôt enrôlé.", + "actions.scoped_workflows.required.expected_contexts": "Signaux attendus (un signal qui correspond au motif est marqué)", + "actions.scoped_workflows.required.no_status_contexts": "Comme cette procédure ne publie aucun signal, la rentre obligatoire empêchera toutes demandes d’ajouts concernées d’être fusionnées. Préférez laisser cette procédure facultative.", + "actions.scoped_workflows.no_files": "Aucune procédure transversale n'a été trouvé dans la branche par défaut.", + "actions.workflow.run": "Réaliser la procédure", + "actions.workflow.create_status_badge": "Créer un badge d’état", + "actions.workflow.status_badge": "Badge d’état", + "actions.workflow.status_badge_url": "URL du badge", + "actions.workflow.not_found": "La procédure « %s » est introuvable.", + "actions.workflow.run_success": "La procédure « %s » est accomplie.", + "actions.workflow.from_ref": "Utiliser la procédure depuis", + "actions.workflow.has_workflow_dispatch": "Cette procédure dispose d’un déclencheur workflow_dispatch.", + "actions.workflow.has_no_workflow_dispatch": "La procédure « %s » n’a pas de déclencheur workflow_dispatch.", + "actions.need_approval_desc": "Une approbation est nécessaire pour exécuter les procédures d‘une demande d’ajout de bifurcation.", + "actions.approve_all_success": "Toutes les procédures ont été approuvées.", "actions.variables": "Variables", "actions.variables.management": "Gestion des variables", "actions.variables.creation": "Ajouter une variable", @@ -3830,7 +3888,7 @@ "actions.general": "Général", "actions.general.enable_actions": "Activer les actions", "actions.general.collaborative_owners_management": "Gestion des collaborateurs", - "actions.general.collaborative_owners_management_help": "Un collaborateur est un utilisateur ou une organisation dont le dépôt privé peut accéder aux actions et flux de travail de ce dépôt.", + "actions.general.collaborative_owners_management_help": "Un collaborateur est un utilisateur ou une organisation dont le dépôt privé peut accéder aux actions et procédures de ce dépôt.", "actions.general.add_collaborative_owner": "Ajouter un collaborateur", "actions.general.collaborative_owner_not_exist": "Le collaborateur n’existe pas.", "actions.general.remove_collaborative_owner": "Supprimer le collaborateur", @@ -3848,21 +3906,21 @@ "git.filemode.symbolic_link": "Lien symbolique", "git.filemode.submodule": "Sous-module", "org.repos.none": "Aucun dépôt.", - "actions.general.permissions": "Permissions du jeton des actions", - "actions.general.token_permissions.mode": "Permissions par défaut du jeton", - "actions.general.token_permissions.mode.desc": "Une tâche d’Actions utilisera les permissions par défaut si aucune n’est déclarée dans le fichier du flux de travail.", - "actions.general.token_permissions.mode.permissive": "Permissif", - "actions.general.token_permissions.mode.permissive.desc": "Permissions en lecture et écriture sur le dépôt de la tâche.", - "actions.general.token_permissions.mode.restricted": "Restreint", - "actions.general.token_permissions.mode.restricted.desc": "Permissions en lecture seule pour le contenu (code, publications) sur le dépôt de la tâche.", - "actions.general.token_permissions.override_owner": "Écraser la configuration faite par le propriétaire", - "actions.general.token_permissions.override_owner_desc": "Si actif, ce dépôt utilisera sa propre configuration pour les actions au lieu de respecter celle du propriétaire (utilisateur ou organisation).", - "actions.general.token_permissions.maximum": "Permissions maximales du jeton", - "actions.general.token_permissions.maximum.description": "Les permissions effectives de la tâche des actions seront limitées par les permissions maximales.", - "actions.general.token_permissions.fork_pr_note": "Si une tâche est démarrée par une demande de fusion depuis une bifurcation, ses permissions effectives ne dépasseront pas les permissions en lecture-seule.", - "actions.general.token_permissions.customize_max_permissions": "Personnaliser les permissions maximales", + "actions.general.permissions": "Permissions intégrées au jeton d’Actions", + "actions.general.token_permissions.mode": "Régime de restriction par défaut", + "actions.general.token_permissions.mode.desc": "Lorsqu’une procédure ne contient pas de restriction, elle est alors restreinte au régime par défaut du dépôt.", + "actions.general.token_permissions.mode.permissive": "Régime permissif", + "actions.general.token_permissions.mode.permissive.desc": "Les missions peuvent consulter et modifier ce dépôt.", + "actions.general.token_permissions.mode.restricted": "Régime limitant", + "actions.general.token_permissions.mode.restricted.desc": "Les missions ne peuvent que consulter le contenu (code et publications) de ce dépôt.", + "actions.general.token_permissions.override_owner": "Outrepasser la configuration du propriétaire", + "actions.general.token_permissions.override_owner_desc": "Si actif, ce dépôt donnera ses propres restrictions à la place de celles du propriétaire (utilisateur ou organisation).", + "actions.general.token_permissions.maximum": "Restrictions affinées", + "actions.general.token_permissions.maximum.description": "Pour protéger plus finement le dépôt, les restrictions appliquées aux missions peuvent être définies pour chaque section individuellement.", + "actions.general.token_permissions.fork_pr_note": "Lorsqu’une mission est initiée par une demande d’ajout depuis une bifurcation, ses restrictions effectives ne peuvent dépasser la lecture seule.", + "actions.general.token_permissions.customize_max_permissions": "Affiner les restrictions", "actions.general.cross_repo": "Accès inter-dépôt", - "actions.general.cross_repo_desc": "Permet aux dépôts sélectionnés d’être visible en lecture-seule par tous les dépôts de ce propriétaire à l’aide de GITEA_TOKEN lors de l’exécution des tâches d’actions.", + "actions.general.cross_repo_desc": "Permet aux dépôts de ce propriétaire de consulter (en lecture seule) les dépôts listés ci-dessous pendant l’exécution des missions d’Actions (nécessite un jeton GITEA_TOKEN).", "actions.general.cross_repo_selected": "Dépôts sélectionnés", "actions.general.cross_repo_target_repos": "Dépôts cibles", "actions.general.cross_repo_add": "Ajouter un dépôt cible" diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index c1e11779d1..9806159383 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -124,6 +124,7 @@ "artifacts": "Déantáin", "expired": "Imithe in éag", "artifact_expires_at": "Éagaíonn ag %s", + "artifact_expired_at": "Imithe in éag ag %s", "confirm_delete_artifact": "An bhfuil tú cinnte gur mian leat an déantán '%s' a scriosadh?", "archived": "Cartlann", "concept_system_global": "Domhanda", @@ -2251,7 +2252,6 @@ "repo.settings.webhook_deletion_success": "Tá an Crúca Gréasán bainte amach.", "repo.settings.webhook.test_delivery": "Imeacht Brúigh Tástála", "repo.settings.webhook.test_delivery_desc": "Déan tástáil ar an webhook seo le teagmhas brú bréige.", - "repo.settings.webhook.test_delivery_desc_disabled": "Chun an Crúca Gréasán seo a thástáil le himeacht bhréige, gníomhachtaigh é.", "repo.settings.webhook.request": "Iarratas", "repo.settings.webhook.response": "Freagra", "repo.settings.webhook.headers": "Ceanntásca", @@ -3306,7 +3306,6 @@ "admin.config.default_enable_timetracking": "Cumasaigh Rianú Ama de réir Réamhshocrú", "admin.config.default_allow_only_contributors_to_track_time": "Lig do Rannpháirtithe Amháin Rianú Am", "admin.config.no_reply_address": "Fearann Ríomhphoist Folaithe", - "admin.config.default_visibility_organization": "Infheictheacht réamhshocraithe d'Eagraíochtaí nua", "admin.config.default_enable_dependencies": "Cumasaigh Spleáchais Eisithe de réir Réamhshocrú", "admin.config.webhook_config": "Cumraíocht Crúca Gréasán", "admin.config.queue_length": "Fad scuaine", @@ -3779,10 +3778,14 @@ "actions.runs.commit": "Tiomantas", "actions.runs.run_details": "Sonraí Rith", "actions.runs.workflow_file": "Comhad sreabhadh oibre", + "actions.runs.workflow_file_no_permission": "Gan cead chun an comhad sreafa oibre a fheiceáil", "actions.runs.scheduled": "Sceidealaithe", "actions.runs.pushed_by": "bhrú ag", "actions.runs.invalid_workflow_helper": "Tá comhad cumraíochta sreabhadh oibre nebhailí. Seiceáil do chomhad cumraithe le do thoil: %s", "actions.runs.no_matching_online_runner_helper": "Gan aon reathaí ar líne a mheaitseáil le lipéad: %s", + "actions.runs.no_runner_online": "Níl aon reathaí ar líne chun an post seo a phiocadh suas.", + "actions.runs.waiting_for_available_runner": "Ag fanacht go mbeidh reathaí comhoiriúnach ar fáil.", + "actions.runs.waiting_for_dependent_jobs": "Ag fanacht go mbeidh na poist seo a leanas críochnaithe: %s", "actions.runs.no_job_without_needs": "Caithfidh post amháin ar a laghad a bheith sa sreabhadh oibre gan spleáchas.", "actions.runs.no_job": "Caithfidh post amháin ar a laghad a bheith sa sreabhadh oibre", "actions.runs.invalid_reusable_workflow_uses": "Sreabhadh oibre in-athúsáidte neamhbhailí \"úsáidí\": %s", @@ -3832,7 +3835,36 @@ "actions.workflow.enable": "Cumasaigh sreabhadh oibre", "actions.workflow.enable_success": "Cumasaíodh sreabhadh oibre '%s' go rathúil.", "actions.workflow.disabled": "Tá sreabhadh oibre díchumasaithe", + "actions.workflow.scope_owner": "Úinéir", + "actions.workflow.scope_global": "Domhanda", + "actions.workflow.required": "Riachtanach", + "actions.workflow.scoped_required_cannot_disable": "Tá an sreabhadh oibre raonaithe seo riachtanach agus ní féidir é a dhíchumasú.", + "actions.scoped_workflows": "Sreafaí Oibre Raonaithe", + "actions.scoped_workflows.desc_org": "Cláraigh stórtha mar fhoinsí sreabhadh oibre raonta. Ritheann comhaid sreabhadh oibre faoi eolairí sreabhadh oibre raonta brainse réamhshocraithe stórtha foinse ar gach stórtha den eagraíocht seo, i gcomhthéacs an stórtha sin féin.", + "actions.scoped_workflows.desc_user": "Cláraigh stórtha mar fhoinsí sreabhadh oibre raonta. Ritheann comhaid sreabhadh oibre faoi eolairí sreabhadh oibre raonta brainse réamhshocraithe stórtha foinse ar gach stór atá i do sheilbh, i gcomhthéacs an stórtha sin féin.", + "actions.scoped_workflows.desc_global": "Cláraigh stórtha mar fhoinsí sreabhadh oibre raonta. Ritheann comhaid sreabhadh oibre faoi eolairí sreabhadh oibre raonta brainse réamhshocraithe stórtha foinse ar gach stór ar an gcás seo, i gcomhthéacs an stórtha sin féin. Ós rud é go ndéantar foinsí ar leibhéal an chás a mheas ar imeachtaí gach stórtha, is féidir le clárú na gcomhad sin forchostais a chur leis ar chásanna móra.", + "actions.scoped_workflows.add_help": "Chun sreafaí oibre raonta a sholáthar ó stór, cuir na comhaid sreafa oibre faoi %s ar a bhrainse réamhshocraithe, agus ansin cláraigh an stór mar fhoinse thíos.", + "actions.scoped_workflows.security_note": "Déantar ábhar sreafa oibre stórais foinse a fhorghníomhú i ngach stórais lena mbaineann sé, agus scríobhtar a scripteanna céime agus a n-aschur chuig logaí Gníomhartha an stórais sin agus is féidir le duine ar bith ar féidir leo Gníomhartha an stórais ídigh a fheiceáil iad a léamh. Dá bhrí sin, nochtar loighic a sreafa oibre trí na logaí sin nuair a chláraítear stórais phríobháidigh mar fhoinse. Ní chláraítear ach stórais a bhféadfadh a n-ábhar sreafa oibre a bheith roinnte le gach stórais ídigh. Má thagraíonn sreabhadh oibre raonaithe do shreabhadh oibre in-athúsáidte ó stórais phríobháidigh, déan cinnte gur féidir le gach stórais ídigh é a léamh, nó teipfidh ar an sreabhadh oibre ansin.", + "actions.scoped_workflows.source.add": "Cuir stór foinse leis", + "actions.scoped_workflows.source.add_success": "Stór foinse curtha leis.", + "actions.scoped_workflows.source.remove_success": "Baineadh an stór foinse.", + "actions.scoped_workflows.source.not_found": "Níor aimsíodh an stórlann.", + "actions.scoped_workflows.required.update_success": "Sreafaí oibre riachtanacha nuashonraithe.", + "actions.scoped_workflows.required.label": "Marcáil sreafaí oibre mar riachtanacha (ní féidir le stórtha sreabhadh oibre riachtanach a dhíchumasú):", + "actions.scoped_workflows.required.patterns": "Patrúin seiceála stádais riachtanacha", + "actions.scoped_workflows.required.patterns_aria": "Patrúin seiceála stádais riachtanacha do %s", + "actions.scoped_workflows.required.patterns_note": "i bhfeidhm ach amháin nuair a bhíonn an sreabhadh oibre ag teastáil", + "actions.scoped_workflows.required.patterns_hint": "Marcáil an sreabhadh oibre mar is gá chun a phatrúin seiceála stádais a chumrú.", + "actions.scoped_workflows.required.patterns_help": "Patrún seiceála stádais amháin (glob) in aghaidh an líne. Ní féidir iarratas tarraingthe íditheach a chumasc ach amháin nuair a bheidh stádas a mheaitseálann gach patrún rite. Cuirtear é seo i bhfeidhm ar aon bhrainse sprice a bhfuil riail chosanta aige, fiú ceann a bhfuil a sheiceálacha stádais féin díchumasaithe; ní dhéantar geataíocht ar bhrainse sprice gan aon riail chosanta.", + "actions.scoped_workflows.required.patterns_empty": "Teastaíonn patrún seiceála stádais amháin ar a laghad ó gach sreabhadh oibre riachtanach.", + "actions.scoped_workflows.required.missing_file": "níl an comhad sa bhunleagan a thuilleadh", + "actions.scoped_workflows.required.expected_contexts": "Seiceálacha stádais ionchais (marcáiltear seic a mheaitseálann patrún)", + "actions.scoped_workflows.required.no_status_contexts": "Ní phostálann an sreabhadh oibre seo aon seiceálacha stádais, mar sin má mharcálann tú é mar riachtanas, chuirfeadh sé bac ar gach iarratas tarraingthe atá á úsáid ó chumasc. Díthiceáil Riachtanach.", + "actions.scoped_workflows.no_files": "Ní bhfuarthas aon chomhaid sreabha oibre raonaithe ar an mbrainse réamhshocraithe.", "actions.workflow.run": "Rith Sreabhadh Oibre", + "actions.workflow.create_status_badge": "Cruthaigh suaitheantas stádais", + "actions.workflow.status_badge": "Suaitheantas Stádais", + "actions.workflow.status_badge_url": "URL suaitheantais", "actions.workflow.not_found": "Níor aimsíodh sreabhadh oibre '%s'.", "actions.workflow.run_success": "Ritheann sreabhadh oibre '%s' go rathúil.", "actions.workflow.from_ref": "Úsáid sreabhadh oibre ó", diff --git a/options/locale/locale_hu-HU.json b/options/locale/locale_hu-HU.json index 4cc29c7f07..dd226f43e0 100644 --- a/options/locale/locale_hu-HU.json +++ b/options/locale/locale_hu-HU.json @@ -122,12 +122,6 @@ "install.register_confirm": "A regisztrációhoz e-mail visszaigazolás szükséges", "install.mail_notify": "Email értesítés engedélyezése", "install.server_service_title": "Szerver és egyéb szolgáltatások beállítása", - "install.offline_mode": "Helyi mód bekapcsolása", - "install.offline_mode_popup": "Gravatar és egyedi források kikapcsolása, minden avatárt a felhasználók töltenek fel.", - "install.disable_gravatar": "Gravatar Kikapcsolása", - "install.disable_gravatar_popup": "Gravatar és a harmadik féltől származó avatar források letoltása. Alapértelmezett avatárt fog használni, kivéve, ha a felhasználó helyileg tölt fel avatárt.", - "install.federated_avatar_lookup": "Összevont profilkép lekérés engedélyezése", - "install.federated_avatar_lookup_popup": "Összevont profilkép lekérés engedélyezése a libravatar használatával.", "install.disable_registration": "Ön-regisztráció kikapcsolása", "install.disable_registration_popup": "Regisztráció kikapcsolása, csak a rendszergazda hozhat létre fiókokat.", "install.allow_only_external_registration_popup": "Regisztráció engedélyezése csak külső forrásokból", @@ -145,10 +139,8 @@ "install.admin_email": "E-mail cím", "install.install_btn_confirm": "Gitea telepítése", "install.test_git_failed": "Nem sikerült a 'git' parancs kipróbálása: %v", - "install.sqlite3_not_available": "Ez a verzió nem támogatja az SQLite3-at, kérlek töltsd le a hivatalos bináris verziót: %s (NE a gobuild változatot).", "install.invalid_db_setting": "Az adatbázis-beállítások érvénytelenek: %v", "install.invalid_repo_path": "Repository gyökérkönyvtára helytelen: %v", - "install.run_user_not_match": "Futtató felhasználó más, mint az aktuális felhasználó: %s -> %s", "install.save_config_failed": "Hiba történt a konfiguráció mentése közben: %v", "install.invalid_admin_setting": "Hibás a rendszergazdai fiók beállítása: %v", "install.invalid_log_root_path": "Naplózás gyökérmappa érvénytelen: %v", @@ -462,7 +454,6 @@ "repo.template_helper": "A tároló legyen sablon", "repo.visibility": "Láthatóság", "repo.visibility_description": "Csak a tulajdonos és a megfelelő jogokkal rendelkező tagok láthatják ezt.", - "repo.visibility_fork_helper": "(Ennek a megváltoztatása kihat minden másolatra)", "repo.clone_helper": "Segítségre van szüksége a klónozáshoz? Látogassa meg a súgót.", "repo.fork_repo": "Tároló lemásolása", "repo.fork_from": "Másolás innen", @@ -608,7 +599,6 @@ "repo.issues.new": "Új hibajegy", "repo.issues.new.title_empty": "A cím nem lehet üres", "repo.issues.new.labels": "Címkék", - "repo.issues.new.no_label": "Nincs címke", "repo.issues.new.clear_labels": "Címkék kiürítése", "repo.issues.new.projects": "Projektek", "repo.issues.new.no_items": "Nincsenek elemek", @@ -680,7 +670,6 @@ "repo.issues.context.quote_reply": "Válasz idézettel", "repo.issues.context.edit": "Szerkesztés", "repo.issues.context.delete": "Törlés", - "repo.issues.reopen_issue": "Újranyitás", "repo.issues.create_comment": "Hozzászólás", "repo.issues.commit_ref_at": "hivatkozott erre a hibajegyre egy commit-ból %[2]s", "repo.issues.author": "Szerző", @@ -767,7 +756,6 @@ "repo.pulls.compare_base": "egyesítés ide", "repo.pulls.compare_compare": "egyesítés innen", "repo.pulls.filter_branch": "Ágra szűrés", - "repo.pulls.nothing_to_compare": "Ezek az ágak egyenlőek. Nincs szükség egyesítési kérésre.", "repo.pulls.create": "Egyesítési kérés létrehozása", "repo.pulls.title_desc": "egyesíteni szeretné %[1]d változás(oka)t a(z) %[2]s-ból %[3]s-ba", "repo.pulls.merged_title_desc": "egyesítve %[1]d változás(ok) a %[2]s-ból %[3]s-ba %[4]s", @@ -945,6 +933,7 @@ "repo.diff.comment.reply": "Válasz", "repo.diff.review": "Értékelés", "repo.diff.review.comment": "Hozzászólás", + "repo.commits.avatar_stack_and": "és", "repo.release.releases": "Kiadások", "repo.release.tags": "Címkék", "repo.release.new_release": "Új kiadás", @@ -1032,7 +1021,6 @@ "org.teams.members": "Csoporttagok", "org.teams.update_settings": "Beállítások frissítése", "org.teams.delete_team": "Csapat törlése", - "org.teams.add_team_member": "Csapattag hozzáadása", "org.teams.delete_team_title": "Csapat törlése", "org.teams.delete_team_desc": "Egy csapat törlése visszavonja a tagjai hozzáférését a tárolókhoz. Folytatható?", "org.teams.delete_team_success": "A csoport törölve lett.", @@ -1047,12 +1035,16 @@ "org.teams.members.none": "Ennek a csapatnak nincsenek tagjai.", "org.teams.specific_repositories": "Meghatározott tárolók", "org.teams.all_repositories": "Minden tároló", + "org.teams.visibility": "Láthatóság", + "org.teams.visibility_private": "Privát", + "org.teams.visibility_public": "Nyilvános", "admin.dashboard": "Műszerfal", "admin.users": "Felhasználói fiókok", "admin.organizations": "Szervezetek", "admin.repositories": "Tárolók", "admin.hooks": "Webhook-ok", "admin.authentication": "Hitelesítési források", + "admin.badges.description": "Leírás", "admin.config": "Konfiguráció", "admin.config_summary": "Összefoglaló", "admin.config_settings": "Beállítások", @@ -1199,10 +1191,8 @@ "admin.config.server_config": "Szerver Konfiguráció", "admin.config.app_name": "Webhely címe", "admin.config.app_ver": "Gitea Verzió", - "admin.config.app_url": "A Gitea alapértelmezett címe", "admin.config.custom_conf": "Konfigurációs fájl elérési útja", "admin.config.custom_file_root_path": "Egyedi fájlok gyökérútvonala", - "admin.config.offline_mode": "Helyi mód", "admin.config.disable_router_log": "Útválasztás naplózásának letiltása", "admin.config.run_user": "Futtatás mint", "admin.config.run_mode": "Futtatás Módja", @@ -1246,7 +1236,6 @@ "admin.config.enable_timetracking": "Időmérés bekapcsolása", "admin.config.default_enable_timetracking": "Időmérés bekapcsolása alapértelmezetten", "admin.config.no_reply_address": "Rejtett e-mail tartomány", - "admin.config.default_visibility_organization": "Alapértelmezett láthatóság új szervezeteknek", "admin.config.webhook_config": "Webhook Beállítása", "admin.config.queue_length": "Várakozási Sor Hossza", "admin.config.deliver_timeout": "Kézbesítési Időtúllépés", @@ -1264,10 +1253,8 @@ "admin.config.cache_config": "Gyorsítótár Beállítások", "admin.config.cache_adapter": "Gyorsítótár Adapter", "admin.config.cache_interval": "Gyorsítótár Intervallum", - "admin.config.cache_conn": "Gyorsítótár Kapcsolat", "admin.config.session_config": "Munkamenet Beállítások", "admin.config.session_provider": "Munkamenet Szolgáltató", - "admin.config.provider_config": "Szolgáltató Beállítás", "admin.config.cookie_name": "Süti Név", "admin.config.gc_interval_time": "GC Intervallum Idő", "admin.config.session_life_time": "Munkamenet Élettartama", @@ -1275,7 +1262,6 @@ "admin.config.cookie_life_time": "Süti Élettartam", "admin.config.picture_config": "Kép és Avatár Konfiguráció", "admin.config.picture_service": "Kép Szolgáltatás", - "admin.config.disable_gravatar": "Gravatar Kikapcsolása", "admin.config.enable_federated_avatar": "Összevont profilkép lekérés engedélyezése", "admin.config.git_config": "Git Beállítás", "admin.config.git_disable_diff_highlight": "Különbségi nézetben a szintakszis kiemelés kikapcsolása", @@ -1285,8 +1271,6 @@ "admin.config.git_gc_args": "Szemétgyűjtő (GC) Argumentumok", "admin.config.git_migrate_timeout": "Migrációs Időtúllépés", "admin.config.git_mirror_timeout": "Tükör Frissítési Időtúllépés", - "admin.config.git_clone_timeout": "Klónozási időtúllépés", - "admin.config.git_pull_timeout": "Frissítési Időtúllépés", "admin.config.git_gc_timeout": "Szemétgyűjtési (GC) Időtúllépés", "admin.config.log_config": "Naplózási Beállítások", "admin.config.disabled_logger": "Letiltva", @@ -1373,6 +1357,8 @@ "packages.container.labels": "Címkék", "packages.npm.dependencies": "Függőségek", "packages.npm.details.tag": "Címke", + "packages.terraform.lock": "Lezárás", + "packages.terraform.unlock": "Feloldás", "packages.owner.settings.cleanuprules.enabled": "Engedélyezett", "secrets.creation.description": "Leírás", "actions.runners.name": "Név", @@ -1383,5 +1369,7 @@ "actions.runners.task_list.repository": "Tároló", "actions.runners.status.active": "Aktív", "actions.runners.version": "Verzió", - "git.filemode.symbolic_link": "Szimbolikus hivatkozás" + "actions.runs.summary": "Összefoglaló", + "actions.workflow.scope_owner": "Tulajdonos", + "actions.general.token_permissions.mode.restricted": "Korlátozott" } diff --git a/options/locale/locale_id-ID.json b/options/locale/locale_id-ID.json index 4a953f04ac..a3d7b76d6e 100644 --- a/options/locale/locale_id-ID.json +++ b/options/locale/locale_id-ID.json @@ -3,7 +3,9 @@ "dashboard": "Dasbor", "explore_title": "Jelajahi", "help": "Bantuan", + "logo": "Logo", "sign_in": "Masuk", + "sign_in_with_provider": "Masuk dengan %s", "sign_in_or": "atau", "sign_out": "Keluar", "sign_up": "Daftar", @@ -18,11 +20,17 @@ "create_new": "Buat…", "user_profile_and_more": "Profil dan Pengaturan…", "signed_in_as": "Masuk sebagai", + "enable_javascript": "Situs web ini memerlukan JavaScript.", "toc": "Daftar Isi", + "licenses": "Lisensi", + "return_to_gitea": "Kembali ke Gitea", + "more_items": "Item lainnya", "username": "Nama Pengguna", "email": "Alamat Email", "password": "Kata Sandi", + "access_token": "Token Akses", "re_type": "Konfirmasi Kata Sandi", + "captcha": "CAPTCHA", "twofa": "Otentikasi Dua Faktor", "twofa_scratch": "Kode Awal Dua Faktor", "passcode": "Kode Akses", @@ -33,6 +41,7 @@ "webauthn_error": "Tidak dapat membaca kunci keamanan Anda.", "webauthn_unsupported_browser": "Browser Anda saat ini tidak mendukung WebAuthn.", "webauthn_error_unknown": "Terdapat kesalahan yang tidak diketahui. Mohon coba lagi.", + "webauthn_error_insecure": "WebAuthn hanya mendukung koneksi aman. Untuk melakukan tes lewat HTTP, anda dapat menggunakan origin \"localhost\" atau \"127.0.0.1\"", "webauthn_error_unable_to_process": "Server tidak dapat memproses permintaan Anda.", "webauthn_error_duplicated": "Kunci keamanan tidak diperbolehkan untuk permintaan ini. Pastikan bahwa kunci ini belum terdaftar sebelumnya.", "webauthn_error_empty": "Anda harus menetapkan nama untuk kunci ini.", @@ -72,6 +81,7 @@ "save": "Simpan", "add": "Tambah", "add_all": "Tambah Semua", + "dismiss": "Abaikan", "remove": "Buang", "remove_all": "Buang Semua", "remove_label_str": "Hapus item \"%s\"", @@ -121,6 +131,7 @@ "no_results_found": "Hasil tidak ditemukan.", "search.type_tooltip": "Tipe pencarian", "search.exact_tooltip": "Hanya menampilkan hasil yang cocok dengan istilah pencarian", + "search.team_kind": "Cari tim…", "search.code_search_unavailable": "Pencarian kode saat ini tidak tersedia. Silahkan hubungi administrator.", "aria.navbar": "Bar Navigasi", "aria.footer.software": "Tentang Software", @@ -164,8 +175,6 @@ "install.smtp_from": "Kirim Email Sebagai", "install.register_confirm": "Perlu Konfirmasi Email Saat Pendaftaran", "install.mail_notify": "Aktifkan Notifikasi Email", - "install.disable_gravatar": "Menonaktifkan Gravatar", - "install.federated_avatar_lookup": "Aktifkan pencarian avatar", "install.openid_signin": "Aktifkan Login OpenID", "install.require_sign_in_view": "Harus Login Untuk Melihat Halaman", "install.admin_password": "Kata Sandi", @@ -418,7 +427,6 @@ "settings.permission_read": "Dibaca", "settings.manage_oauth2_applications": "Kelola Aplikasi OAuth2", "settings.edit_oauth2_application": "Sunting Aplikasi OAuth2", - "settings.oauth2_applications_desc": "Aplikasi OAuth2 memungkinkan aplikasi pihak ketiga Anda untuk autentikasi pengguna pada instans Gitea ini dengan aman.", "settings.remove_oauth2_application": "Hapus Aplikasi OAuth2", "settings.remove_oauth2_application_desc": "Menghapus aplikasi OAuth2 akan mencabut akses ke semua token akses yang tertandatangani. Lanjutkan?", "settings.remove_oauth2_application_success": "Aplikasi telah dihapus.", @@ -431,7 +439,6 @@ "settings.oauth2_regenerate_secret": "Buat Ulang Rahasia", "settings.oauth2_regenerate_secret_hint": "Anda kehilangan rahasia?", "settings.oauth2_application_edit": "Sunting", - "settings.oauth2_application_create_description": "Aplikasi OAuth2 memberikan aplikasi pihak ketiga Anda akses akun pengguna pada instans ini.", "settings.authorized_oauth2_applications": "Aplikasi OAuth2 Terotorisasi", "settings.revoke_key": "Cabut", "settings.revoke_oauth2_grant": "Cabut Akses", @@ -471,7 +478,6 @@ "repo.visibility": "Jarak pandang", "repo.visibility_description": "Hanya pemilik, atau anggota dari organisasi ini yang punya akses, yang dapat melihatnya.", "repo.visibility_helper_forced": "Admin situs Anda memaksa repositori baru menjadi pribadi.", - "repo.visibility_fork_helper": "(Mengubah hal ini akan mempengaruhi semua garpu.)", "repo.clone_helper": "Butuh bantuan kloning? Kunjungi Bantuan.", "repo.fork_repo": "Cabang Gudang penyimpanan", "repo.fork_from": "Cabang Dari", @@ -590,7 +596,6 @@ "repo.projects.column.new": "Kolom Baru", "repo.issues.new": "Masalah Baru", "repo.issues.new.labels": "Label", - "repo.issues.new.no_label": "Tidak ada Label", "repo.issues.new.clear_labels": "Label yang jelas", "repo.issues.new.milestone": "Tolak ukur waktu", "repo.issues.new.no_milestone": "Tidak Ada Milestone", @@ -643,7 +648,6 @@ "repo.issues.context.quote_reply": "Kutip Balasan", "repo.issues.context.edit": "Sunting", "repo.issues.context.delete": "Hapus", - "repo.issues.reopen_issue": "Buka kembali", "repo.issues.create_comment": "Komentar", "repo.issues.commit_ref_at": "merujuk masalah dari komit %[2]s", "repo.issues.author": "Penulis", @@ -786,8 +790,6 @@ "repo.settings.webhook.payload": "Konten", "repo.settings.webhook.body": "Tubuh", "repo.settings.githook_edit_desc": "Jika hook tidak aktif, konten sampel akan dipaparkan. Meninggalkan konten dengan nilai kosong akan menonaktifkan hook ini.", - "repo.settings.githook_name": "Nama Hook", - "repo.settings.githook_content": "Konten Hook", "repo.settings.update_githook": "Perbarui Hook", "repo.settings.secret": "Rahasia", "repo.settings.slack_username": "Nama pengguna", @@ -885,12 +887,15 @@ "org.teams.settings": "Pengaturan", "org.teams.members": "Anggota tim", "org.teams.update_settings": "Memperbarui pengaturan", - "org.teams.add_team_member": "Tambahkan Anggota Tim", "org.teams.delete_team_success": "Tim sudah di hapus.", "org.teams.repositories": "Tim repositori", + "org.teams.visibility": "Jarak pandang", + "org.teams.visibility_private": "Pribadi", + "org.teams.visibility_public": "Publik", "admin.dashboard": "Dasbor", "admin.organizations": "Organisasi", "admin.repositories": "Repositori", + "admin.badges.description": "Deskripsi", "admin.config": "Konfigurasi", "admin.config_settings": "Pengaturan", "admin.notices": "Pemberitahuan Sistem", @@ -1044,10 +1049,8 @@ "admin.config.cache_config": "Konfigurasi cache", "admin.config.cache_adapter": "Adaptor cache", "admin.config.cache_interval": "Interval cache", - "admin.config.cache_conn": "Koneksi cache", "admin.config.session_config": "Sesi konfigurasi", "admin.config.session_provider": "Sesi penyedia", - "admin.config.provider_config": "Konfigurasi penyedia", "admin.config.cookie_name": "Nama cookie", "admin.config.gc_interval_time": "Waktu interval GC", "admin.config.session_life_time": "Sesi jangka waktu", @@ -1055,7 +1058,6 @@ "admin.config.cookie_life_time": "Jangka waktu cookie", "admin.config.picture_config": "Pengaturan Foto dan Avatar", "admin.config.picture_service": "Gambar layanan", - "admin.config.disable_gravatar": "Menonaktifkan Gravatar", "admin.config.enable_federated_avatar": "Aktifkan pencarian avatar", "admin.config.git_config": "Konfigurasi Git", "admin.config.git_disable_diff_highlight": "Menonaktifkan Diff Syntax utama", @@ -1065,8 +1067,6 @@ "admin.config.git_gc_args": "Argumen GC", "admin.config.git_migrate_timeout": "Batas Waktu Migrasi", "admin.config.git_mirror_timeout": "Waktu habis untuk memperbarui cermin", - "admin.config.git_clone_timeout": "Waktu habis operasi klon", - "admin.config.git_pull_timeout": "Waktu habis tarik operasi", "admin.config.git_gc_timeout": "Waktu habis operasi GC", "admin.config.log_config": "Catat konfigurasi", "admin.config.disabled_logger": "Nonaktif", @@ -1180,6 +1180,7 @@ "actions.workflow.enable": "Aktifkan Alur Kerja", "actions.workflow.enable_success": "Alur kerja '%s' berhasil diaktifkan.", "actions.workflow.disabled": "Alur kerja dinonaktifkan.", + "actions.workflow.scope_owner": "Pemilik", "actions.need_approval_desc": "Butuh persetujuan untuk menjalankan alur kerja untuk pull request fork.", "actions.variables": "Variabel", "actions.variables.management": "Managemen Variabel", diff --git a/options/locale/locale_is-IS.json b/options/locale/locale_is-IS.json index 33f1691446..b64cfecca3 100644 --- a/options/locale/locale_is-IS.json +++ b/options/locale/locale_is-IS.json @@ -154,11 +154,6 @@ "install.register_confirm": "Krefjast Staðfestingar Tölvupósts Til Að Nýskrá", "install.mail_notify": "Virkja Tölvupósttilkynningar", "install.server_service_title": "Stillingar Netþjóns og Þriðja Aðila", - "install.offline_mode": "Virkjaðu Staðbundin Ham", - "install.offline_mode_popup": "Slökktu á efnisafhendingarnetum þriðja aðila og þjónaðu öllum gögnum á staðnum.", - "install.disable_gravatar": "Óvirkja Gravatar", - "install.disable_gravatar_popup": "Slökkva á Gravatar og notandamyndar þjónustum. Sjálfgefin notandamynd verður notuð ef notandi hleður ekki upp sína eigin.", - "install.federated_avatar_lookup": "Virkja Samtök Notandamyndar", "install.openid_signin": "Virkja OpenID Innskráningu", "install.openid_signin_popup": "Virkja OpenID innskráningu notenda.", "install.enable_captcha": "Virkja CAPTCHA innskráningu", @@ -168,7 +163,6 @@ "install.admin_email": "Netfang", "install.install_btn_confirm": "Setja upp Gitea", "install.test_git_failed": "Gat ekki prófað „git“ skipunina: %v", - "install.sqlite3_not_available": "Þessi Gitea útgáfa styður ekki SQLite3. Vinsamlegast sæktu útgáfunni okkar frá %s (ekki „gobuild“ útgáfunna).", "install.invalid_db_setting": "Gagnagrunnsstillingarnar eru ógildar: %v", "install.invalid_repo_path": "Grunnsslóð hugbúnaðarsafns er ógild: %v", "install.invalid_log_root_path": "Slóð annáls er ógild: %v", @@ -449,7 +443,6 @@ "repo.owner": "Eigandi", "repo.template": "Sniðmát", "repo.visibility": "Sýnileiki", - "repo.visibility_fork_helper": "(Að breyta þessu mun hafa áhrif á allar skiptingar.)", "repo.clone_helper": "Þarftu hjálp við afritun? Fáðu aðstoð.", "repo.fork_repo": "Tvískipta Hugbúnaðarsafni", "repo.download_zip": "Sækja ZIP", @@ -591,11 +584,9 @@ "repo.issues.filter_labels": "Sía Lýsingar", "repo.issues.new": "Nýtt Vandamál", "repo.issues.new.labels": "Lýsingar", - "repo.issues.new.no_label": "Engin Lýsing", "repo.issues.new.clear_labels": "Tæma lýsingar", "repo.issues.new.projects": "Verkefni", "repo.issues.new.clear_projects": "Tæma Verkefnum", - "repo.issues.new.no_projects": "Ekkert verkefni", "repo.issues.new.open_projects": "Opin Verkefni", "repo.issues.new.closed_projects": "Lokuð Verkefni", "repo.issues.new.milestone": "Tímamót", @@ -646,7 +637,6 @@ "repo.issues.commented_at": "gerði ummæli %s", "repo.issues.context.edit": "Breyta", "repo.issues.context.delete": "Eyða", - "repo.issues.reopen_issue": "Enduropna", "repo.issues.create_comment": "Senda Ummæli", "repo.issues.closed_at": "lokaði þessu vandamáli %[2]s", "repo.issues.reopened_at": "enduropnaði þetta vandamál %[2]s", @@ -900,6 +890,7 @@ "repo.diff.comment.add_review_comment": "Leggja inn ummæli", "repo.diff.comment.reply": "Svara", "repo.diff.review.comment": "Senda Ummæli", + "repo.commits.avatar_stack_and": "og", "repo.diff.image.overlay": "Yfirleggja", "repo.release.releases": "Útgáfur", "repo.release.tags": "Merki", @@ -951,10 +942,13 @@ "org.teams.settings": "Stillingar", "org.teams.update_settings": "Uppfæra Stillingar", "org.teams.all_repositories": "Öll hugbúnaðarsöfn", + "org.teams.visibility": "Sýnileiki", + "org.teams.visibility_public": "Opinbert", "admin.dashboard": "Stjórnborð", "admin.organizations": "Stofnanir", "admin.repositories": "Hugbúnaðarsöfn", "admin.hooks": "Vefkrókar", + "admin.badges.description": "Lýsing", "admin.config": "Stilling", "admin.config_summary": "Yfirlit", "admin.config_settings": "Stillingar", @@ -1015,8 +1009,6 @@ "admin.auths.tips": "Ábendingar", "admin.config.app_name": "Heiti Vefsvæðis", "admin.config.app_ver": "Útgáfu Gitea", - "admin.config.app_url": "Grunnvefslóð Gitea", - "admin.config.domain": "Lén Netþjóns", "admin.config.run_user": "Keyra Sem Notandanafn", "admin.config.git_version": "Útgáfa Git", "admin.config.repo_root_path": "Grunnsslóð Hugbúnaðarsafns", @@ -1039,7 +1031,6 @@ "admin.config.mailer_smtp_port": "SMTP Gátt", "admin.config.mailer_user": "Notandi", "admin.config.https_only": "Aðeins HTTPS", - "admin.config.disable_gravatar": "Óvirkja Gravatar", "admin.config.enable_federated_avatar": "Virkja Samtök Notandamyndar", "admin.monitor.name": "Heiti", "admin.monitor.desc": "Lýsing", @@ -1105,6 +1096,8 @@ "packages.npm.dependencies": "Kröfur", "packages.npm.details.tag": "Merki", "packages.pypi.requires": "Þarfnast Python", + "packages.terraform.lock": "Læsa", + "packages.terraform.unlock": "Aflæsa", "secrets.creation.description": "Lýsing", "actions.runners.id": "Auðkenni", "actions.runners.name": "Heiti", @@ -1116,5 +1109,9 @@ "actions.runners.task_list.commit": "Framlag", "actions.runners.status.active": "Virkt", "actions.runners.version": "Útgáfa", - "actions.runs.commit": "Framlag" + "actions.runs.commit": "Framlag", + "actions.runs.branch": "Grein", + "actions.runs.summary": "Yfirlit", + "actions.workflow.scope_owner": "Eigandi", + "actions.workflow.required": "Nauðsynlegt" } diff --git a/options/locale/locale_it-IT.json b/options/locale/locale_it-IT.json index 1a2b86b148..981a5565d2 100644 --- a/options/locale/locale_it-IT.json +++ b/options/locale/locale_it-IT.json @@ -144,12 +144,6 @@ "install.register_confirm": "Richiedere la conferma Email per registrarsi", "install.mail_notify": "Attiva le notifiche Email", "install.server_service_title": "Impostazioni Server e Servizi di Terza Parte", - "install.offline_mode": "Attiva la Modalità in Locale", - "install.offline_mode_popup": "Disattiva le reti di distribuzione dei contenuti di terze parti e fornisci tutte le risorse localmente.", - "install.disable_gravatar": "Disattiva Gravatar", - "install.disable_gravatar_popup": "Disattiva Gravatar e le fonti di avatar di terze parti. Verrà usato un avatar predefinito almeno che un utente non carichi un avatar in locale.", - "install.federated_avatar_lookup": "Attiva i Federated Avatar", - "install.federated_avatar_lookup_popup": "Enable federated avatars lookup to use federated open source service based on libravatar.", "install.disable_registration": "Disattiva Self-Registration", "install.disable_registration_popup": "Disattiva la user self-registration. Solo gli amministratori saranno in grado di creare account.", "install.allow_only_external_registration_popup": "Attiva la registrazione solo tramite servizi esterni", @@ -167,11 +161,9 @@ "install.admin_email": "Indirizzo Email", "install.install_btn_confirm": "Installare Gitea", "install.test_git_failed": "Fallito il test del comando git: %v", - "install.sqlite3_not_available": "Questa versione di Gitea non supporta SQLite3. Si prega di scaricare la versione binaria ufficiale da %s (not the 'gobuild' version).", "install.invalid_db_setting": "Le impostazioni del database sono invalide: %v", "install.invalid_repo_path": "Il percorso radice del Repository è invalido: %v", "install.invalid_app_data_path": "Il percorso dati dell'app non è valido: %v", - "install.run_user_not_match": "Il nome utente 'esegui come' non è il nome utente attuale: %s -> %s", "install.internal_token_failed": "Generazione del token interno non riuscita: %v", "install.secret_key_failed": "Generazione della chiave segreta non riuscita: %v", "install.save_config_failed": "Salvataggio della configurazione non riuscito: %v", @@ -549,7 +541,6 @@ "settings.permission_read": "Lette", "settings.manage_oauth2_applications": "Gestisci applicazioni OAuth2", "settings.edit_oauth2_application": "Modifica applicazione OAuth2", - "settings.oauth2_applications_desc": "OAuth2 consente alle applicazione di terze parti di autenticarsi in sicurezza a questa istanza di Gitea.", "settings.remove_oauth2_application": "Rimuovi applicazione OAuth2", "settings.remove_oauth2_application_desc": "La rimozione di OAuth2 revocherà l'accesso a tutti i token di accesso autorizzati. Continuare?", "settings.remove_oauth2_application_success": "L'applicazione è stata eliminata.", @@ -560,7 +551,6 @@ "settings.oauth2_regenerate_secret": "Rigenera codice segreto", "settings.oauth2_regenerate_secret_hint": "Perso il codice segreto?", "settings.oauth2_application_edit": "Modifica", - "settings.oauth2_application_create_description": "OAuth2 da l'accesso al tuo account di questa istanza ad applicazioni di terze parti.", "settings.authorized_oauth2_applications": "Applicazioni OAuth2 autorizzate", "settings.revoke_key": "Revoca", "settings.revoke_oauth2_grant": "Revoca accesso", @@ -611,7 +601,6 @@ "repo.visibility": "Visibilità", "repo.visibility_description": "Solo il proprietario o i membri dell'organizzazione se hanno diritti, saranno in grado di vederlo.", "repo.visibility_helper_forced": "L'amministratore del sito impone che le nuove repository siano private.", - "repo.visibility_fork_helper": "(Questa modifica avrà effetto su tutti i fork)", "repo.clone_helper": "Hai bisogno di aiuto per la clonazione? Visita Help.", "repo.fork_repo": "Forka Repository", "repo.fork_from": "Forka da", @@ -833,14 +822,12 @@ "repo.editor.push_rejected": "La modifica è stata rifiutata dal server. Controlla Git Hooks.", "repo.editor.push_rejected_summary": "Messaggio Di Rifiuto Completo:", "repo.editor.add_subdir": "Aggiungi una directory…", - "repo.editor.no_commit_to_branch": "Impossibile effettuare il commit direttamente sul branch perché:", "repo.editor.user_no_push_to_branch": "L'utente non può effettuare il push sul branch", "repo.editor.require_signed_commit": "Il branch richiede un commit firmato", "repo.editor.cherry_pick": "Cherry-pick %s suto:", "repo.editor.revert": "Ripristina %s su:", "repo.commits.desc": "Sfoglia la cronologia di modifiche del codice rogente.", "repo.commits.commits": "Commit", - "repo.commits.nothing_to_compare": "Questi rami sono uguali.", "repo.commits.search_all": "Tutti i branch", "repo.commits.author": "Autore", "repo.commits.message": "Messaggio", @@ -889,11 +876,9 @@ "repo.issues.new": "Nuovo Problema", "repo.issues.new.title_empty": "L'intestazione non può essere vuota", "repo.issues.new.labels": "Etichette", - "repo.issues.new.no_label": "Nessuna etichetta", "repo.issues.new.clear_labels": "Pulisci le etichette", "repo.issues.new.projects": "Progetti", "repo.issues.new.clear_projects": "Cancella progetti", - "repo.issues.new.no_projects": "Nessun progetto", "repo.issues.new.open_projects": "Apri Progetti", "repo.issues.new.closed_projects": "Progetti chiusi", "repo.issues.new.no_items": "Nessun elemento", @@ -990,7 +975,6 @@ "repo.issues.context.reference_issue": "Fai riferimento in un nuovo problema", "repo.issues.context.edit": "Modifica", "repo.issues.context.delete": "Elimina", - "repo.issues.reopen_issue": "Riapri", "repo.issues.create_comment": "Commento", "repo.issues.closed_at": "ha chiuso questo problema %[2]s", "repo.issues.reopened_at": "riaperto questo problema %[2]s", @@ -1143,7 +1127,6 @@ "repo.pulls.allow_edits_from_maintainers": "Consenti modifiche dai manutentori", "repo.pulls.allow_edits_from_maintainers_desc": "Gli utenti con accesso in scrittura al ramo base possono anche inviare a questo ramo", "repo.pulls.allow_edits_from_maintainers_err": "Aggiornamento non riuscito", - "repo.pulls.compare_changes_desc": "Selezione il branch su cui eseguire il merge e il branch da cui eseguire il pull.", "repo.pulls.has_viewed_file": "Visualizzato", "repo.pulls.has_changed_since_last_review": "Modificato dalla tua ultima recensione", "repo.pulls.viewed_files_label": "%[1]d / %[2]d file visti", @@ -1152,7 +1135,6 @@ "repo.pulls.switch_comparison_type": "Cambia tipo di confronto", "repo.pulls.switch_head_and_base": "Testa e base di commutazione", "repo.pulls.filter_branch": "Filtra branch", - "repo.pulls.nothing_to_compare": "Questi rami sono uguali. Non c'è alcuna necessità di creare una pull request.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "Questi rami sono uguali. Questa PR sarà vuota.", "repo.pulls.has_pull_request": "Una pull request tra questi rami esiste già: %[2]s#%[3]d", "repo.pulls.create": "Crea Pull Request", @@ -1207,7 +1189,6 @@ "repo.pulls.status_checking": "Alcuni controlli sono in sospeso", "repo.pulls.status_checks_success": "Tutti i controlli sono stati effettuati con successo", "repo.pulls.status_checks_warning": "Alcuni controlli hanno segnalato avvertimenti", - "repo.pulls.status_checks_failure": "Alcuni controlli sono falliti", "repo.pulls.status_checks_error": "Alcuni controlli hanno segnalato errori", "repo.pulls.status_checks_requested": "Richiesto", "repo.pulls.status_checks_details": "Dettagli", @@ -1410,7 +1391,6 @@ "repo.settings.transfer_abort": "Annulla trasferimento", "repo.settings.transfer_abort_invalid": "Non è possibile annullare un trasferimento di repository non esistente.", "repo.settings.transfer_desc": "Trasferisci questo repository a un altro utente o a un'organizzazione nella quale hai diritti d'amministratore.", - "repo.settings.transfer_form_title": "Inserisci il nome del repository come conferma:", "repo.settings.transfer_notices_1": "-Si perderà l'accesso al repository se lo si trasferisce ad un utente singolo.", "repo.settings.transfer_notices_2": "-Si manterrà l'accesso al repository se si trasferisce in un'organizzazione che possiedi (o condividi con qualcun'altro).", "repo.settings.transfer_notices_3": "- Se il repository è privato e viene trasferito a un singolo utente, questa azione si assicura che l'utente abbia almeno i permessi di lettura (e le modifiche se necessario).", @@ -1469,8 +1449,6 @@ "repo.settings.webhook.delivery.success": "Un evento è stato aggiunto alla coda di consegna. Potrebbe volerci qualche secondo prima che venga visualizzato nella cronologia delle consegne.", "repo.settings.githooks_desc": "Git Hooks è alimentato da Git stesso. È possibile modificare i file hook qui sotto per impostare operazioni personalizzate.", "repo.settings.githook_edit_desc": "Se l'hook è inattivo, sarà presentato un contenuto esempio. Lasciando il contenuto vuoto disattiverai questo hook.", - "repo.settings.githook_name": "Nome hook", - "repo.settings.githook_content": "Contenuto hook", "repo.settings.update_githook": "Aggiorna Hook", "repo.settings.add_webhook_desc": "Gitea invierà richieste POST con un tipo di contenuto specifico all'URL di destinazione. Per saperne di più leggi la guida ai webhook.", "repo.settings.payload_url": "URL di destinazione", @@ -1576,7 +1554,6 @@ "repo.settings.block_on_official_review_requests_desc": "Il merge non sarà possibile quando sono richiesti cambiamenti da revisori, anche se ci sono sufficienti approvazioni.", "repo.settings.block_outdated_branch": "Blocca il merge se la pull request è obsoleta", "repo.settings.block_outdated_branch_desc": "Il merging non sarà possibile quando il ramo testa è dietro il ramo base.", - "repo.settings.default_branch_desc": "Seleziona un branch del repository predefinito per le pull request ed i commit di codice:", "repo.settings.choose_branch": "Scegli un branch…", "repo.settings.no_protected_branch": "Non ci sono branch protetti.", "repo.settings.edit_protected_branch": "Modifica", @@ -1657,8 +1634,6 @@ "repo.diff.too_many_files": "Alcuni file non sono stati mostrati perché troppi file sono cambiati in questo diff", "repo.diff.show_more": "Mostra Altro", "repo.diff.load": "Carica Diff", - "repo.diff.generated": "generato", - "repo.diff.vendored": "esterno", "repo.diff.comment.placeholder": "Lascia un commento", "repo.diff.comment.add_single_comment": "Aggiungi un commento singolo", "repo.diff.comment.add_review_comment": "Aggiungi commento", @@ -1673,6 +1648,7 @@ "repo.diff.review.reject": "Richiedi cambiamenti", "repo.diff.review.self_approve": "Gli autori di Pull request non possono approvare la loro pull request", "repo.diff.committed_by": "committato da", + "repo.commits.avatar_stack_and": "e", "repo.diff.protected": "Protetto", "repo.diff.image.side_by_side": "A fianco", "repo.diff.image.swipe": "Scorri", @@ -1784,9 +1760,7 @@ "org.settings.labels_desc": "Aggiungi i webhooks che verranno attivati per tutti i repository sotto questa organizzazione.", "org.members.membership_visibility": "Visibilità appartenenza:", "org.members.public": "Visibile", - "org.members.public_helper": "nascondi", "org.members.private": "Nascosto", - "org.members.private_helper": "rendi visibile", "org.members.member_role": "Ruolo del membro:", "org.members.owner": "Proprietario", "org.members.member": "Membro", @@ -1816,7 +1790,6 @@ "org.teams.members": "Membri del Team", "org.teams.update_settings": "Aggiorna Impostazioni", "org.teams.delete_team": "Elimina team", - "org.teams.add_team_member": "Aggiungi un Membro al Team", "org.teams.delete_team_title": "Elimina team", "org.teams.delete_team_desc": "Eliminare un team revocherà l'accesso al repository da parte dei suoi membri. Continuare?", "org.teams.delete_team_success": "Il team è stato eliminato.", @@ -1839,11 +1812,16 @@ "org.teams.all_repositories_read_permission_desc": "Questo team concede permessi lettura l'accesso a tutte le repository: i membri possono visualizzare e clonare i repository.", "org.teams.all_repositories_write_permission_desc": "Questo team concede permessi di scrittura accesso a tutte le repository: i membri possono leggere e pushare le repository.", "org.teams.all_repositories_admin_permission_desc": "Questo team concede a Amministratore l'accesso a tutte le repository: i membri possono leggere, pushare e aggiungere collaboratori alle repository.", + "org.teams.visibility": "Visibilità", + "org.teams.visibility_private": "Privati", + "org.teams.visibility_limited": "Limitato", + "org.teams.visibility_public": "Pubblico", "admin.dashboard": "Pannello di Controllo", "admin.users": "Account utenti", "admin.organizations": "Organizzazioni", "admin.repositories": "Repository", "admin.authentication": "Fonti di autenticazione", + "admin.badges.description": "Descrizione", "admin.config": "Configurazione", "admin.config_summary": "Riepilogo", "admin.config_settings": "Impostazioni", @@ -2086,11 +2064,8 @@ "admin.config.server_config": "Configurazione Server", "admin.config.app_name": "Titolo del Sito", "admin.config.app_ver": "Versione Gitea", - "admin.config.app_url": "URL di base di Gitea", "admin.config.custom_conf": "Percorso file di configurazione", "admin.config.custom_file_root_path": "Percorso Root File Personalizzato", - "admin.config.domain": "Dominio Server", - "admin.config.offline_mode": "Modalità locale", "admin.config.disable_router_log": "Disattivare Log del Router", "admin.config.run_user": "Esegui come Nome utente", "admin.config.run_mode": "Modalità Esecuzione", @@ -2137,7 +2112,6 @@ "admin.config.default_enable_timetracking": "Attiva il cronografo di Default", "admin.config.default_allow_only_contributors_to_track_time": "Consenti soltanto ai contributori di utilizzare il cronografo", "admin.config.no_reply_address": "Dominio email nascosto", - "admin.config.default_visibility_organization": "Visibilità predefinita per le nuove organizzazioni", "admin.config.default_enable_dependencies": "Abilita i problemi delle dipendenze di default", "admin.config.webhook_config": "Configurazione Webhook", "admin.config.queue_length": "Lunghezza della coda", @@ -2161,11 +2135,9 @@ "admin.config.cache_config": "Configurazione Cache", "admin.config.cache_adapter": "Adattatore Cache", "admin.config.cache_interval": "Intervallo Cache", - "admin.config.cache_conn": "Connessione Cache", "admin.config.cache_item_ttl": "Cache degli elementi TTL", "admin.config.session_config": "Configurazione Sessione", "admin.config.session_provider": "Fornitore Sessione", - "admin.config.provider_config": "Impostazioni Provider", "admin.config.cookie_name": "Nome del Cookie", "admin.config.gc_interval_time": "Intervallo di tempo della GC", "admin.config.session_life_time": "Durata Sessione", @@ -2173,7 +2145,6 @@ "admin.config.cookie_life_time": "Durata Cookie", "admin.config.picture_config": "Configurazione Immagine profilo e Avatar", "admin.config.picture_service": "Servizio foto", - "admin.config.disable_gravatar": "Disabilita Gravatar", "admin.config.enable_federated_avatar": "Attiva i Federated Avatar", "admin.config.git_config": "Configurazione Git", "admin.config.git_disable_diff_highlight": "Disattiva evidenziatore Diff Syntax", @@ -2183,8 +2154,6 @@ "admin.config.git_gc_args": "Parametri GC", "admin.config.git_migrate_timeout": "Timeout per la migrazione", "admin.config.git_mirror_timeout": "Timeout per l'aggiornamento del mirror", - "admin.config.git_clone_timeout": "Tempo limite operazione di clone", - "admin.config.git_pull_timeout": "Tempo limite operazione di pull", "admin.config.git_gc_timeout": "Timeout operazione GC", "admin.config.log_config": "Configurazione Log", "admin.config.disabled_logger": "Disabilitato", @@ -2358,6 +2327,9 @@ "packages.rubygems.dependencies.development": "Dipendenze Di Sviluppo", "packages.rubygems.required.ruby": "Richiede la versione di Ruby", "packages.rubygems.required.rubygems": "Richiede la versione RubyGem", + "packages.terraform.lock_status": "Stato Blocco", + "packages.terraform.lock": "Blocca", + "packages.terraform.unlock": "Sblocca", "packages.settings.link": "Collega questo pacchetto a un repository", "packages.settings.link.select": "Seleziona Repository", "packages.settings.link.button": "Aggiorna Collegamento Repository", @@ -2377,5 +2349,9 @@ "actions.runners.task_list.run": "Esegui", "actions.runners.status.active": "Attivo", "actions.runners.version": "Versione", - "git.filemode.symbolic_link": "Link Simbolico" + "actions.runs.branch": "Ramo", + "actions.runs.summary": "Riepilogo", + "actions.workflow.scope_owner": "Proprietario", + "actions.workflow.required": "Richiesto", + "actions.general.token_permissions.mode.restricted": "Limitato" } diff --git a/options/locale/locale_ja-JP.json b/options/locale/locale_ja-JP.json index 4af9d945b2..44292c6561 100644 --- a/options/locale/locale_ja-JP.json +++ b/options/locale/locale_ja-JP.json @@ -1321,6 +1321,7 @@ "repo.editor.fork_branch_exists": "ブランチ \"%s\" は既にフォークに存在します。新しいブランチ名を選択してください。", "repo.commits.desc": "ソースコードの変更履歴を参照します。", "repo.commits.commits": "コミット", + "repo.commits.history_enable_follow_renames": "リネームも含める", "repo.commits.no_commits": "共通のコミットはありません。 \"%s\" と \"%s\" の履歴はすべて異なっています。", "repo.commits.nothing_to_compare": "差分はありません。", "repo.commits.search.tooltip": "キーワード \"author:\"、\"committer:\"、\"after:\"、\"before:\" を付けて指定できます。 例 \"revert author:Alice before:2019-01-13\"", @@ -2204,10 +2205,10 @@ "repo.settings.trust_model.collaborator.desc": "このリポジトリの共同作業者による正常な署名は、署名がコミッターのものかどうかにかかわらず「信頼済み」とみなします。 それ以外で正常な署名がコミッターのものであれば「信頼不可」、そうでなければ「不一致」となります。", "repo.settings.trust_model.committer": "コミッター", "repo.settings.trust_model.committer.long": "コミッター: コミッターに一致する署名を信頼します。 これはGitHub方式であり、Giteaの署名が付いたコミットはコミッターがGiteaであることが強制されます。", - "repo.settings.trust_model.committer.desc": "正常な署名は、コミッターに一致する場合のみ「信頼済み」とみなし、それ以外は「不一致」となります。 Giteaは署名付きでコミットすることが強制され、本来のコミッターはコミットの最後に Co-authored-by: と Co-committed-by: で追加されます。 Giteaのデフォルト鍵はデータベース内のユーザー1人とマッチしなければなりません。", + "repo.settings.trust_model.committer.desc": "正常な署名は、コミッターに一致する場合のみ「信頼済み」とみなし、それ以外は「不一致」となります。 Giteaは署名付きでコミットすることが強制され、本来のコミッターはコミットの最後に Co-authored-by: で追加されます。 Giteaのデフォルト鍵はデータベース内のユーザー1人とマッチしなければなりません。", "repo.settings.trust_model.collaboratorcommitter": "共同作業者+コミッター", "repo.settings.trust_model.collaboratorcommitter.long": "共同作業者+コミッター: コミッターと一致する共同作業者による署名を信頼します", - "repo.settings.trust_model.collaboratorcommitter.desc": "このリポジトリの共同作業者による正常な署名は、コミッターと一致する場合に「信頼済み」とみなします。 それ以外の正常な署名のうち、コミッターに一致するものは「信頼不可」、他は「不一致」となります。 Giteaが署名付きコミットのコミッターであることが強制され、本来のコミッターはコミットの最後に Co-Authored-By: と Co-Committed-By: で追加されます。 Giteaのデフォルト鍵はデータベース内のユーザー1人とマッチしなければなりません。", + "repo.settings.trust_model.collaboratorcommitter.desc": "このリポジトリの共同作業者による正常な署名は、コミッターと一致する場合に「信頼済み」とみなします。 それ以外の正常な署名のうち、コミッターに一致するものは「信頼不可」、他は「不一致」となります。 Giteaが署名付きコミットのコミッターであることが強制され、本来のコミッターはコミットの最後に Co-Authored-By: で追加されます。 Giteaのデフォルト鍵はデータベース内のユーザー1人とマッチしなければなりません。", "repo.settings.wiki_delete": "Wikiデータの削除", "repo.settings.wiki_delete_desc": "Wikiデータの削除は恒久的で元に戻すことはできません。", "repo.settings.wiki_delete_notices_1": "- この操作はリポジトリ %s のWikiを恒久的に削除して無効にします。", @@ -2250,7 +2251,6 @@ "repo.settings.webhook_deletion_success": "Webhookを削除しました。", "repo.settings.webhook.test_delivery": "プッシュ・イベントによるテスト", "repo.settings.webhook.test_delivery_desc": "ダミーのプッシュ・イベントでこのWebhookをテストします。", - "repo.settings.webhook.test_delivery_desc_disabled": "このWebhookをダミーのイベントでテストするには、有効にしてください。", "repo.settings.webhook.request": "リクエスト", "repo.settings.webhook.response": "レスポンス", "repo.settings.webhook.headers": "ヘッダー", @@ -2598,6 +2598,9 @@ "repo.diff.review.reject": "変更要請", "repo.diff.review.self_approve": "プルリクエストの作成者は自分のプルリクエストを承認できません", "repo.diff.committed_by": "committed by", + "repo.diff.coauthored_by": "co-authored by", + "repo.commits.avatar_stack_and": "と", + "repo.commits.avatar_stack_people": "%d人", "repo.diff.protected": "保護されているファイル", "repo.diff.image.side_by_side": "並べて表示", "repo.diff.image.swipe": "スワイプ", @@ -2725,6 +2728,7 @@ "graphs.code_frequency.what": "コード更新頻度", "graphs.contributors.what": "実績", "graphs.recent_commits.what": "最近のコミット", + "graphs.chart_zoom_hint": "ドラッグ:ズーム、シフト+ドラッグ:パン、ダブルクリック:ズームをリセット", "org.org_name_holder": "組織名", "org.org_full_name_holder": "組織のフルネーム", "org.org_name_helper": "組織名は短く覚えやすいものにしましょう。", @@ -2860,6 +2864,14 @@ "org.teams.all_repositories_read_permission_desc": "このチームはすべてのリポジトリ読み取りアクセス権を持ちます: メンバーはリポジトリの閲覧とクローンが可能です。", "org.teams.all_repositories_write_permission_desc": "このチームはすべてのリポジトリ書き込みアクセス権を持ちます: メンバーはリポジトリの読み取りとプッシュが可能です。", "org.teams.all_repositories_admin_permission_desc": "このチームはすべてのリポジトリ管理者アクセス権を持ちます: メンバーはリポジトリの読み取り、プッシュ、共同作業者の追加が可能です。", + "org.teams.visibility": "公開/非公開", + "org.teams.visibility_private": "プライベート", + "org.teams.visibility_private_helper": "チームメンバーと組織のオーナーにのみ表示されます。", + "org.teams.visibility_limited": "限定", + "org.teams.visibility_limited_helper": "この組織のすべてのメンバーに表示されます。", + "org.teams.visibility_public": "公開", + "org.teams.visibility_public_helper": "サインインしているすべてのユーザーに表示されます。", + "org.teams.owners_visibility_fixed": "Ownersチームの公開範囲は変更できません。", "org.teams.invite.title": "あなたは組織 %[2]s 内のチーム %[1]s への参加に招待されました。", "org.teams.invite.by": "%s からの招待", "org.teams.invite.description": "下のボタンをクリックしてチームに参加してください。", @@ -3293,7 +3305,6 @@ "admin.config.default_enable_timetracking": "デフォルトでタイムトラッキング有効", "admin.config.default_allow_only_contributors_to_track_time": "コントリビューターだけタイムトラッキングする", "admin.config.no_reply_address": "メールを隠すときのドメイン", - "admin.config.default_visibility_organization": "新しい組織のデフォルトの表示設定", "admin.config.default_enable_dependencies": "デフォルトでイシューの依存関係有効", "admin.config.webhook_config": "Webhook設定", "admin.config.queue_length": "キューの長さ", @@ -3759,21 +3770,27 @@ "actions.runners.reset_registration_token_confirm": "現在のトークンを無効にして、新しいトークンを生成しますか?", "actions.runners.reset_registration_token_success": "ランナー登録トークンをリセットしました", "actions.runs.all_workflows": "すべてのワークフロー", + "actions.runs.other_workflows": "その他のワークフロー", + "actions.runs.other_workflows_tooltip": "このリポジトリで実行されたことがあるが、デフォルトブランチには存在していないワークフロー。", "actions.runs.workflow_run_count_1": "%d 件のワークフロー実行", "actions.runs.workflow_run_count_n": "%d 件のワークフロー実行", "actions.runs.commit": "コミット", "actions.runs.run_details": "実行の詳細", "actions.runs.workflow_file": "ワークフローのファイル", + "actions.runs.workflow_file_no_permission": "ワークフローファイルを表示する権限がありません", "actions.runs.scheduled": "スケジュール済み", "actions.runs.pushed_by": "pushed by", "actions.runs.invalid_workflow_helper": "ワークフロー設定ファイルは無効です。あなたの設定ファイルを確認してください: %s", "actions.runs.no_matching_online_runner_helper": "ラベルに一致するオンラインのランナーが見つかりません: %s", "actions.runs.no_job_without_needs": "ワークフローには依存関係のないジョブが少なくとも1つ含まれている必要があります。", "actions.runs.no_job": "ワークフローには少なくとも1つのジョブが含まれている必要があります", + "actions.runs.invalid_reusable_workflow_uses": "無効な再利用可能ワークフロー \"uses\": %s", "actions.runs.actor": "アクター", "actions.runs.status": "ステータス", "actions.runs.actors_no_select": "すべてのアクター", "actions.runs.status_no_select": "すべてのステータス", + "actions.runs.branch": "ブランチ", + "actions.runs.branches_no_select": "すべてのブランチ", "actions.runs.no_results": "一致する結果はありません。", "actions.runs.no_workflows": "ワークフローはまだありません。", "actions.runs.no_workflows.quick_start": "Gitea Actions の始め方がわからない? ではクイックスタートガイドをご覧ください。", @@ -3788,17 +3805,62 @@ "actions.runs.view_workflow_file": "ワークフローファイルを表示", "actions.runs.summary": "サマリー", "actions.runs.all_jobs": "すべてのジョブ", + "actions.runs.job_summaries": "ジョブの概要", + "actions.runs.expand_caller_jobs": "再利用可能ワークフロー呼び出しのジョブを表示", + "actions.runs.collapse_caller_jobs": "再利用可能ワークフロー呼び出しのジョブを非表示", "actions.runs.attempt": "試行", "actions.runs.latest": "最新", "actions.runs.latest_attempt": "最新 試行", "actions.runs.triggered_via": "%s によりトリガー", - "actions.runs.total_duration": "総所要時間:", + "actions.runs.rerun_triggered": "再実行をトリガー", + "actions.runs.back_to_pull_request": "プルリクエストに戻る", + "actions.runs.back_to_workflow": "ワークフローに戻る", + "actions.runs.total_duration": "合計時間", + "actions.runs.workflow_dependencies": "ワークフローの依存関係", + "actions.runs.graph_jobs_count_1": "%d 件のジョブ", + "actions.runs.graph_jobs_count_n": "%d 件のジョブ", + "actions.runs.graph_dependencies_count_1": "%d 件の依存関係", + "actions.runs.graph_dependencies_count_n": "%d 件の依存関係", + "actions.runs.graph_success_rate": "%s 成功", + "actions.runs.graph_zoom_in": "ズームイン(グラフ上で Ctrl/Cmd + スクロール)", + "actions.runs.graph_zoom_max": "既に 100% ズーム", + "actions.runs.graph_zoom_out": "ズームアウト(グラフ上で Ctrl/Cmd + スクロール)", + "actions.runs.graph_reset_view": "表示をリセット", "actions.workflow.disable": "ワークフローを無効にする", "actions.workflow.disable_success": "ワークフロー '%s' が無効になりました。", "actions.workflow.enable": "ワークフローを有効にする", "actions.workflow.enable_success": "ワークフロー '%s' が有効になりました。", "actions.workflow.disabled": "ワークフローは無効です。", + "actions.workflow.scope_owner": "オーナー", + "actions.workflow.scope_global": "グローバル", + "actions.workflow.required": "必須", + "actions.workflow.scoped_required_cannot_disable": "このスコープ付きワークフローは必須で、無効にできません。", + "actions.scoped_workflows": "スコープ付きワークフロー", + "actions.scoped_workflows.desc_org": "スコープ付きワークフローのソースとなるリポジトリを登録します。 ソースリポジトリのデフォルトブランチ内で、スコープ付きワークフロー用ディレクトリにワークフローファイルを置くと、組織内のすべてのリポジトリで、それぞれのリポジトリ自身のコンテキストで実行されます。", + "actions.scoped_workflows.desc_user": "スコープ付きワークフローのソースとなるリポジトリを登録します。 ソースリポジトリのデフォルトブランチ内で、スコープ付きワークフロー用ディレクトリにワークフローファイルを置くと、あなたが所有するすべてのリポジトリで、それぞれのリポジトリ自身のコンテキストで実行されます。", + "actions.scoped_workflows.desc_global": "スコープ付きワークフローのソースとなるリポジトリを登録します。 ソースリポジトリのデフォルトブランチ内で、スコープ付きワークフロー用ディレクトリにワークフローファイルを置くと、このインスタンスのすべてのリポジトリで、それぞれのリポジトリ自身のコンテキストで実行されます。 インスタンスレベルのソースはあらゆるリポジトリでイベントが発生するたびに評価されるため、大規模インスタンスで登録するとオーバーヘッドが増加する可能性があります。", + "actions.scoped_workflows.add_help": "スコープ付きワークフローをリポジトリから提供するには、デフォルトブランチの %s にワークフローファイルをコミットし、以下でそのリポジトリをソースとして登録します。", + "actions.scoped_workflows.security_note": "ソースリポジトリのワークフローの内容は、適用対象となるすべてのリポジトリ内で実行されます。 ステップごとのスクリプトとその出力は、実行したリポジトリのActionsログに書き込まれ、そのリポジトリのActionsを閲覧できるユーザーであれば誰でも読むことができます。 プライベートリポジトリをソースとして登録しても、そのワークフローのロジックはログを通して開示されることになります。 すべての適用先リポジトリとワークフローの内容を共有しても問題ないリポジトリだけ登録してください。 スコープ付きワークフローが、プライベートリポジトリの再利用可能ワークフローを参照している場合は、すべての適用先リポジトリがそのワークフローを読み取れるようにしてください。 そうでない場合、そこでのワークフローは失敗します。", + "actions.scoped_workflows.source.add": "ソースリポジトリを追加", + "actions.scoped_workflows.source.add_success": "ソースリポジトリを追加しました。", + "actions.scoped_workflows.source.remove_success": "ソースリポジトリを削除しました。", + "actions.scoped_workflows.source.not_found": "リポジトリが見つかりません。", + "actions.scoped_workflows.required.update_success": "必須ワークフローを更新しました。", + "actions.scoped_workflows.required.label": "ワークフローを必須としてマークする (必須ワークフローはリポジトリから無効にすることはできません):", + "actions.scoped_workflows.required.patterns": "必須ステータスチェックパターン", + "actions.scoped_workflows.required.patterns_aria": "%s の必須ステータスチェックパターン", + "actions.scoped_workflows.required.patterns_note": "ワークフローが必須の場合にだけ適用されます", + "actions.scoped_workflows.required.patterns_hint": "ステータスチェックパターンを設定するには、ワークフローを必須とマークします。", + "actions.scoped_workflows.required.patterns_help": "ステータスチェックのパターン(glob)を1行につき1つずつ記入します。 対象となるプルリクエストは、ステータスがすべてのパターンに一致して初めてマージ可能になります。 これは保護ルールを持つターゲットブランチであれば、そのステータスチェックが無効になっていたとしても適用されます。 一方、保護ルールを持たないターゲットブランチは制限されません。", + "actions.scoped_workflows.required.patterns_empty": "必須ワークフローには、少なくともひとつのステータスチェックパターンが必要です。", + "actions.scoped_workflows.required.missing_file": "ファイルがもうソースにありません", + "actions.scoped_workflows.required.expected_contexts": "想定されるステータスチェック (パターンに一致するチェックがマークされています)", + "actions.scoped_workflows.required.no_status_contexts": "このワークフローのステータスチェックが送られてきていないため、必須ワークフローにすると、適用されるプルリクエストのマージをすべてブロックしてしまいます。 必須を解除してください。", + "actions.scoped_workflows.no_files": "デフォルトブランチに、スコープ付きワークフローのファイルが見つかりません。", "actions.workflow.run": "ワークフローを実行", + "actions.workflow.create_status_badge": "ステータスバッジを作成する", + "actions.workflow.status_badge": "ステータスバッジ", + "actions.workflow.status_badge_url": "バッジURL", "actions.workflow.not_found": "ワークフロー '%s' が見つかりません。", "actions.workflow.run_success": "ワークフロー '%s' は正常に実行されました。", "actions.workflow.from_ref": "使用するワークフローの取得元", diff --git a/options/locale/locale_ko-KR.json b/options/locale/locale_ko-KR.json index 121d0fb8bf..5fb2211761 100644 --- a/options/locale/locale_ko-KR.json +++ b/options/locale/locale_ko-KR.json @@ -1584,7 +1584,7 @@ "repo.issues.label_archived_filter": "아카이빙된 레이블 표시", "repo.issues.label_archive_tooltip": "아카아빙된 레이블은 레이블로 검색할 때 기본적으로 제안 목록에서 제외됩니다.", "repo.issues.label_exclusive_desc": "레이블명을 스코프/항목으로 지정하여 다른 스코프/ 레이블과 상호 배타적으로 만드세요.", - "repo.issues.label_exclusive_warning": "이슈 또는 풀 리퀘스트의 레이블을 편집할 때 충돌하는 스코프 지정 레이블은 모두 제거됩니다.", + "repo.issues.label_exclusive_warning": "이슈 또는 풀 리퀘스트의 레이블을 편집할 때 충돌하는 범위지정 레이블은 모두 제거됩니다.", "repo.issues.label_exclusive_order": "정렬 순서", "repo.issues.label_exclusive_order_tooltip": "같은 스코프 내의 독점 레이블은 이 숫자 순서에 따라 정렬됩니다.", "repo.issues.label_count": "레이블 %d개", @@ -2204,10 +2204,8 @@ "repo.settings.trust_model.collaborator.desc": "이 리포지토리 공동작업자에 의한 유효한 서명은 커미터와 일치하는지 여부에 관계없이 \"신뢰됨\"으로 표시됩니다. 그렇지 않으면 유효한 서명이 커미터와 일치하면 \"신뢰할 수 없음\"으로, 일치하지 않으면 \"불일치\"로 마킹됩니다.", "repo.settings.trust_model.committer": "커미터", "repo.settings.trust_model.committer.long": "커미터: 커미터와 일치하는 신뢰 서명을 허용합니다. 이는 GitHub의 동작과 일치하며, Gitea가 서명한 커밋은 Gitea를 커미터로 강제 지정합니다.", - "repo.settings.trust_model.committer.desc": "유효한 서명은 커미터와 일치하는 경우에만 \"신뢰됨\"으로 마킹되며, 그렇지 않으면 \"불일치\"로 마킹됩니다. 이는 실제 커미터가 커밋에서 Co-authored-by: 및 Co-committed-by: 트레일러로 표시된 서명된 커밋에 Gitea를 커미터로 강제합니다. 기본 Gitea 키는 데이터베이스의 사용자와 일치해야 합니다.", "repo.settings.trust_model.collaboratorcommitter": "공동작업자+커미터", "repo.settings.trust_model.collaboratorcommitter.long": "공동작업자+커미터: 커미터와 일치하는 공동작업자의 서명을 신뢰", - "repo.settings.trust_model.collaboratorcommitter.desc": "이 리포지토리 공동작업자의 유효한 서명은 커미터와 일치하는 경우 \"신뢰됨\"으로 마킹됩니다. 그렇지 않으면 유효한 서명이 커미터와 일치하면 \"신뢰할 수 없음\"으로, 그렇지 않으면 \"불일치\"로 마킹됩니다. 이렇게 하면 Gitea는 실제 커미터가 커밋에 Co-Authored-By: 및 Co-Committed-By: 트레일러로 표시된 서명된 커밋의 커미터로 강제됩니다. 기본 Gitea 키는 데이터베이스의 사용자와 일치해야 합니다.", "repo.settings.wiki_delete": "위키 데이터 삭제", "repo.settings.wiki_delete_desc": "리포지토리 위키 데이터를 삭제하는 것은 영구적이며 되돌릴 수 없습니다.", "repo.settings.wiki_delete_notices_1": "- 이 작업은 %s의 리포지토리 위키를 영구적으로 삭제하고 비활성화합니다.", @@ -2250,7 +2248,6 @@ "repo.settings.webhook_deletion_success": "Webhook을 삭제했습니다.", "repo.settings.webhook.test_delivery": "푸시 이벤트 테스트", "repo.settings.webhook.test_delivery_desc": "가짜 푸시 이벤트로 웹훅을 테스트합니다.", - "repo.settings.webhook.test_delivery_desc_disabled": "가짜 이벤트로 이 웹훅을 테스트하려면 활성화하십시오.", "repo.settings.webhook.request": "요청", "repo.settings.webhook.response": "응답", "repo.settings.webhook.headers": "제목", @@ -2598,6 +2595,7 @@ "repo.diff.review.reject": "변경 요청", "repo.diff.review.self_approve": "풀 리퀘스트 작성자는 자신의 풀 리퀘스트를 승인할 수 없습니다", "repo.diff.committed_by": "커밋한 사람", + "repo.commits.avatar_stack_and": "그리고", "repo.diff.protected": "보호됨", "repo.diff.image.side_by_side": "나란히 보기", "repo.diff.image.swipe": "스와이프", @@ -2861,6 +2859,10 @@ "org.teams.all_repositories_read_permission_desc": "이 팀은 모든 리포지토리에 대한 읽기 액세스 권한을 부여합니다: 멤버는 리포지토리를 보고 클로닝할 수 있습니다.", "org.teams.all_repositories_write_permission_desc": "이 팀은 모든 리포지토리에 대한 쓰기 액세스 권한을 부여합니다: 멤버는 리포지토리에 읽고 푸시할 수 있습니다.", "org.teams.all_repositories_admin_permission_desc": "이 팀은 모든 리포지토리에 대한 운영자 액세스 권한을 부여합니다: 멤버는 리포지토리에 읽기, 푸시, 공동작업자 추가를 할 수 있습니다.", + "org.teams.visibility": "공개범위", + "org.teams.visibility_private": "비공개", + "org.teams.visibility_limited": "제한됨", + "org.teams.visibility_public": "공개", "org.teams.invite.title": "당신은 조직 %s의 팀 %s에 초대되었습니다.", "org.teams.invite.by": "%s에 의해 초대됨", "org.teams.invite.description": "팀에 참여하려면 아래 버튼을 클릭하십시오.", @@ -3294,7 +3296,6 @@ "admin.config.default_enable_timetracking": "기본 타임 트래킹 활성화", "admin.config.default_allow_only_contributors_to_track_time": "기여자 트랙 타임만", "admin.config.no_reply_address": "답변 받지 않을 이메일 주소", - "admin.config.default_visibility_organization": "새 조직의 기본 공개범위", "admin.config.default_enable_dependencies": "기본적으로 이슈 종속성을 활성화", "admin.config.webhook_config": "웹훅 설정", "admin.config.queue_length": "큐 길이", @@ -3799,7 +3800,6 @@ "actions.runs.latest": "최신", "actions.runs.latest_attempt": "최근 시도", "actions.runs.triggered_via": "%s를 통해 트리거됨", - "actions.runs.total_duration": "총기간:", "actions.runs.workflow_dependencies": "워크플로우 의존성", "actions.runs.graph_jobs_count_1": "%d 작업", "actions.runs.graph_jobs_count_n": "%d 작업", @@ -3815,7 +3815,15 @@ "actions.workflow.enable": "워크플로 활성화", "actions.workflow.enable_success": "워크플로 '%s'가 성공적으로 활성화되었습니다.", "actions.workflow.disabled": "워크플로가 비활성화되었습니다.", + "actions.workflow.scope_owner": "소유자", + "actions.workflow.scope_global": "글로벌", + "actions.workflow.required": "필수 항목", + "actions.workflow.scoped_required_cannot_disable": "범위지정 워크플로우가 요구되며 비활성화할 수 없습니다.", + "actions.scoped_workflows": "범위지정 워크플로우", "actions.workflow.run": "워크플로 실행", + "actions.workflow.create_status_badge": "상태 배지 생성", + "actions.workflow.status_badge": "상태 배지", + "actions.workflow.status_badge_url": "배지 URL", "actions.workflow.not_found": "워크플로 '%s'를 찾을 수 없습니다.", "actions.workflow.run_success": "워크플로 '%s'가 성공적으로 실행되었습니다.", "actions.workflow.from_ref": "다음에서 워크플로 사용", diff --git a/options/locale/locale_lv-LV.json b/options/locale/locale_lv-LV.json index 136ae13644..2420ead997 100644 --- a/options/locale/locale_lv-LV.json +++ b/options/locale/locale_lv-LV.json @@ -189,7 +189,6 @@ "install.lfs_path": "Git LFS glabāšanas vieta", "install.lfs_path_helper": "Faili, kas pievienoti Git LFS, tiks glabāti šajā direktorijā. Atstājiet tukšu, lai atspējotu.", "install.run_user": "Izpildes lietotājs", - "install.run_user_helper": "Operētājsistēms lietotājs, ar kuru tiks palaists Gitea. Jāņem vērā, ka šim lietotājam ir jābūt piekļuvei repozitorija atrašanās vietai.", "install.domain": "Servera domēns", "install.domain_helper": "Domēns vai servera adrese.", "install.ssh_port": "SSH servera ports", @@ -210,12 +209,6 @@ "install.register_confirm": "Reģistrējoties pieprasīt apstiprināt e-pastu", "install.mail_notify": "Iespējot e-pasta paziņojumus", "install.server_service_title": "Servera un citu servisu iestatījumi", - "install.offline_mode": "Iespējot bezsaistes režīmu", - "install.offline_mode_popup": "Atspējot ārējos satura piegādes tīklus, lai visi resursi tiktu piegādāti lokāli.", - "install.disable_gravatar": "Atspējot Gravatar", - "install.disable_gravatar_popup": "Atspējot Gravatar un citus avotus, visus avatarus augšupielādēs lietotāji vai izmantos noklusēto attēlu.", - "install.federated_avatar_lookup": "Iespējot apvienotās profila bildes", - "install.federated_avatar_lookup_popup": "Iespējot apvienoto profila bilžu meklētāju, lai izmantotu atvērtā koda apvienoto servisu balstītu uz Libravatar.", "install.disable_registration": "Atspējot lietotāju reģistrāciju", "install.disable_registration_popup": "Atspējot iespēju reģistrēties. Tikai administratori varēs izveidot jaunus kontus.", "install.allow_only_external_registration_popup": "Atļaut reģistrēties tikai ar ārējiem servisiem", @@ -235,12 +228,10 @@ "install.admin_email": "E-pasta adrese", "install.install_btn_confirm": "Instalēt Gitea", "install.test_git_failed": "Kļūda pārbaudot 'git' komandu: %v", - "install.sqlite3_not_available": "Jūsu pašreizējā versija neatbalsta SQLite3, lūdzu lejupielādējiet oficiālo bināro versiju no %s, NEVIS gobuild versiju.", "install.invalid_db_setting": "Nederīgi datu bāzes iestatījumi: %v", "install.invalid_db_table": "Datubāzes tabula \"%s\" ir kļūdaina: %v", "install.invalid_repo_path": "Nederīga repozitorija glabāšanas vieta: %v", "install.invalid_app_data_path": "Lietojumprogrammas datu ceļš ir kļūdains: %v", - "install.run_user_not_match": "Izpildes lietotājs nav pašreizējais lietotājs: %s -> %s", "install.internal_token_failed": "Neizdevās uzģenerēt iekšējās saziņas pilnvaru: %v", "install.secret_key_failed": "Neizdevās uzģenerēt drošības atslēgu: %v", "install.save_config_failed": "Neizdevās saglabāt konfigurāciju: %v", @@ -694,7 +685,6 @@ "settings.permissions_list": "Tiesības:", "settings.manage_oauth2_applications": "Pārvaldīt OAuth2 lietotnes", "settings.edit_oauth2_application": "Labot OAuth2 lietotni", - "settings.oauth2_applications_desc": "OAuth2 lietotnes ļauj trešo pušu lietotnēm droša veidā autentificēt lietotajus šajā Gitea instancē.", "settings.remove_oauth2_application": "Noņemt OAuth2 lietotni", "settings.remove_oauth2_application_desc": "Noņemot OAuth2 lietotni, tiks noņemta piekļuve visām parakstītajām piekļuves pilnvarām. Vai turpināt?", "settings.remove_oauth2_application_success": "Lietotne tika dzēsta.", @@ -711,7 +701,6 @@ "settings.oauth2_regenerate_secret_hint": "Pazaudēts noslēpums?", "settings.oauth2_client_secret_hint": "Pēc šīs lapas pamešanas vai atsvaidzināšanas noslēpums vairs netiks parādīts. Lūgums pārliecināties, ka tas ir saglabāts.", "settings.oauth2_application_edit": "Labot", - "settings.oauth2_application_create_description": "OAuth2 lietotnes ļauj trešas puses lietotnēm piekļūt lietotāja kontiem šajā instancē.", "settings.oauth2_application_remove_description": "OAuth2 lietotnes noņemšana liegs tai piekļūt pilnvarotiem lietotāju kontiem šajā instancē. Vai turpināt?", "settings.oauth2_application_locked": "Gitea sāknēšanas brīdī reģistrē dažas OAuth2 lietotnes, ja tas ir iespējots konfigurācijā. Lai novērstu negaidītu uzvedību, tās nevar ne labot, ne noņemt. Lūgums vērsties OAuth2 dokumentācijā pēc vairāk informācijas.", "settings.authorized_oauth2_applications": "Autorizētās OAuth2 lietotnes", @@ -777,7 +766,6 @@ "repo.visibility_description": "Tikai organizācijas īpašnieks vai tās biedri, kam ir tiesības, varēs piekļūt šim repozitorijam.", "repo.visibility_helper": "Padarīt repozitoriju privātu", "repo.visibility_helper_forced": "Jūsu sistēmas administrators ir noteicis, ka visiem no jauna izveidotajiem repozitorijiem ir jābūt privātiem.", - "repo.visibility_fork_helper": "(Šīs vērtības maiņa ietekmēs arī visus atdalītos repozitorijus.)", "repo.clone_helper": "Nepieciešama palīdzība klonēšanā? Apmeklē palīdzības sadaļu.", "repo.fork_repo": "Atdalīt repozitoriju", "repo.fork_from": "Atdalīt no", @@ -849,8 +837,6 @@ "repo.blame.ignore_revs.failed": "Neizdevās neņemt vērā izmaiņas no .git-blam-ignore-revs.", "repo.transfer.accept": "Apstiprināt īpašnieka maiņu", "repo.transfer.reject": "Noraidīt īpašnieka maiņu", - "repo.transfer.no_permission_to_accept": "Nav atļaujas pieņemt šo pārsūtīšanu.", - "repo.transfer.no_permission_to_reject": "Nav atļaujas noraidīt šo pārsūtīšanu.", "repo.desc.private": "Privāts", "repo.desc.public": "Publisks", "repo.desc.template": "Sagatave", @@ -1054,7 +1040,6 @@ "repo.editor.upload_file_is_locked": "Failu \"%s\" ir nobloķējis %s.", "repo.editor.upload_files_to_dir": "Augšupielādēt failus uz direktoriju \"%s\"", "repo.editor.cannot_commit_to_protected_branch": "Nav atļauts veikt izmaiņas aizsargātam atzaram \"%s\".", - "repo.editor.no_commit_to_branch": "Nevar apstiprināt revīzijas atzarā:", "repo.editor.user_no_push_to_branch": "Lietotājs nevar iesūtīt izmaiņas šajā atzarā", "repo.editor.require_signed_commit": "Atzarā var iesūtīt tikai parakstītas revīzijas", "repo.editor.cherry_pick": "Izlasīt %s uz:", @@ -1062,7 +1047,6 @@ "repo.commits.desc": "Pārlūkot pirmkoda izmaiņu vēsturi.", "repo.commits.commits": "Revīzijas", "repo.commits.no_commits": "Nav kopīgu revīziju. Atzariem \"%s\" un \"%s\" ir pilnībā atšķirīga izmaiņu vēsture.", - "repo.commits.nothing_to_compare": "Atzari ir vienādi.", "repo.commits.search.tooltip": "Jūs varat izmantot atslēgas vārdus \"author:\", \"committer:\", \"after:\" vai \"before:\", piemēram, \"revert author:Alice before:2019-01-13\".", "repo.commits.search_all": "Visi atzari", "repo.commits.author": "Autors", @@ -1132,11 +1116,9 @@ "repo.issues.new": "Jauna problēma", "repo.issues.new.title_empty": "Nosaukums nevar būt tukšs", "repo.issues.new.labels": "Iezīmes", - "repo.issues.new.no_label": "Nav iezīmju", "repo.issues.new.clear_labels": "Noņemt iezīmes", "repo.issues.new.projects": "Projekti", "repo.issues.new.clear_projects": "Notīrīt projektus", - "repo.issues.new.no_projects": "Nav projektu", "repo.issues.new.open_projects": "Aktīvie projekti", "repo.issues.new.closed_projects": "Pabeigtie projekti", "repo.issues.new.no_items": "Nav vienumu", @@ -1252,7 +1234,6 @@ "repo.issues.close": "Slēgt problēmu", "repo.issues.comment_pull_merged_at": "saplidināta revīzija %[1]s atzarā %[2]s %[3]s", "repo.issues.comment_manually_pull_merged_at": "manuāli saplidināta revīzija %[1]s atzarā %[2]s %[3]s", - "repo.issues.reopen_issue": "Atvērt atkārtoti", "repo.issues.create_comment": "Komentēt", "repo.issues.closed_at": "slēdza šo problēmu %[2]s", "repo.issues.reopened_at": "atkārtoti atvēra šo problēmu %[2]s", @@ -1436,7 +1417,6 @@ "repo.pulls.allow_edits_from_maintainers": "Atļaut labojumus no uzturētājiem", "repo.pulls.allow_edits_from_maintainers_desc": "Lietotāji ar rakstīšanas tiesībām bāzes atzarā, drīkst iesūtīt izmaiņas šajā atzarā", "repo.pulls.allow_edits_from_maintainers_err": "Atjaunošana neizdevās", - "repo.pulls.compare_changes_desc": "Izvēlieties atzaru, kurā sapludināt izmaiņas un atzaru, no kura tās saņemt.", "repo.pulls.has_viewed_file": "Skatīts", "repo.pulls.has_changed_since_last_review": "Mainīts kopš pēdējās recenzijas", "repo.pulls.viewed_files_label": "apskatīts %[1]d no %[2]d failiem", @@ -1453,7 +1433,6 @@ "repo.pulls.showing_specified_commit_range": "Rāda tikai izmaiņas starp %[1]s..%[2]s", "repo.pulls.review_only_possible_for_full_diff": "Recenzēšana ir iespējama tikai tad, kad tiek apskatīts pilns salīdzinājums", "repo.pulls.filter_changes_by_commit": "Atlasīt pēc revīzijas", - "repo.pulls.nothing_to_compare": "Nav ko salīdzināt, jo bāzes un salīdzināmie atzari ir vienādi.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "Šie atzari ir vienādi. Izveidotais izmaiņu pieprasījums būs tukšs.", "repo.pulls.has_pull_request": "Izmaiņu pieprasījums starp šiem atzariem jau eksistē: %[2]s#%[3]d", "repo.pulls.create": "Izveidot izmaiņu pieprasījumu", @@ -1516,7 +1495,6 @@ "repo.pulls.status_checking": "Dažas pārbaudes vēl tiek veiktas", "repo.pulls.status_checks_success": "Visas pārbaudes ir veiksmīgas", "repo.pulls.status_checks_warning": "Dažas pārbaudes ziņoja brīdinājumus", - "repo.pulls.status_checks_failure": "Dažas pārbaudes neizdevās izpildīt", "repo.pulls.status_checks_error": "Dažu pārbaužu izpildes laikā, radās kļūdas", "repo.pulls.status_checks_requested": "Obligāts", "repo.pulls.status_checks_details": "Papildu informācija", @@ -1587,7 +1565,6 @@ "repo.signing.wont_sign.headsigned": "Sapludināšanas revīzija netiks parakstīta, jo galvenā revīzija nav parakstīta.", "repo.signing.wont_sign.commitssigned": "Sapludināšana netiks parakstīta, jo visas saistītās revīzijas nav parakstītas.", "repo.signing.wont_sign.approved": "Sapludināšana netiks parakstīta, jo izmaiņu pieprasījums nav apstiprināts.", - "repo.signing.wont_sign.not_signed_in": "Jūs neesat pieteicies.", "repo.ext_wiki": "Piekļuve ārējai vikivietnei", "repo.ext_wiki.desc": "Ārējā vikivietne norāda uz ārējo vikivietnes adresi.", "repo.wiki": "Vikivietne", @@ -1783,7 +1760,6 @@ "repo.settings.transfer_abort_invalid": "Nevar atcelt neeksistējoša repozitorija īpašnieka maiņu.", "repo.settings.transfer_abort_success": "Repozitorija īpašnieka maiņa uz %s tika veiksmīgi atcelta.", "repo.settings.transfer_desc": "Mainīt šī repozitorija īpašnieku uz citu lietotāju vai organizāciju, kurai Jums ir administratora tiesības.", - "repo.settings.transfer_form_title": "Ievadiet repozitorija nosaukumu, lai apstiprinātu:", "repo.settings.transfer_notices_1": "- Tiks zaudēta piekļuve repozitorijam, ja jaunais īpašnieks ir individuāls lietotājs.", "repo.settings.transfer_notices_2": "- Tiks saglabāta piekļuve, ja jaunais īpašnieks ir organizācija un esat viens no tās īpašniekiem.", "repo.settings.transfer_notices_3": "- Ja repozitorijs ir privāts un tas tiks pārsūtīts lietotājam, tad pārliecināties, ka lietotājam ir vismaz skatīšanās tiesības (veiciet nepieciešamās izmaiņas, ja nepieciešams).", @@ -1836,7 +1812,6 @@ "repo.settings.webhook_deletion": "Noņemt tīmekļa āķi", "repo.settings.webhook_deletion_desc": "Noņemot tīmekļa āķi, tiks dzēsti visi tā iestatījumi un piegādes vēsture. Vai turpināt?", "repo.settings.webhook_deletion_success": "Tīmekļa āķis tika noņemts.", - "repo.settings.webhook.test_delivery_desc_disabled": "Lai pārbaudītu šo tīmekļa āķi ar neīstu notikumu, tas ir jāiespējo.", "repo.settings.webhook.request": "Pieprasījums", "repo.settings.webhook.response": "Atbilde", "repo.settings.webhook.headers": "Galvenes", @@ -1847,8 +1822,6 @@ "repo.settings.webhook.delivery.success": "Notikums tika veiksmīgi pievienots piegādes rindai. Var paiet vairākas sekundes līdz tas parādās piegādes vēsturē.", "repo.settings.githooks_desc": "Git āķus apstrādā pats Git. Jūs varat labot atbalstīto āku failus sarakstā zemāk, lai veiktu pielāgotas darbības.", "repo.settings.githook_edit_desc": "Ja āķis nav aktīvs, tiks attēlots piemērs kā to izmantot. Atstājot āķa saturu tukšu, tas tiks atspējots.", - "repo.settings.githook_name": "Āķa nosaukums", - "repo.settings.githook_content": "Āķa saturs", "repo.settings.update_githook": "Labot āķi", "repo.settings.add_webhook_desc": "Uz norādīto URL tiks nosūtīts POST pieprasījums ar notikuma datiem. Detalizētāku informāciju ir iespējams uzzināt tīmekļa āķu rokasgrāmatā.", "repo.settings.payload_url": "Saņēmēja URL", @@ -1978,7 +1951,6 @@ "repo.settings.block_on_official_review_requests_desc": "Sapludināšana nebūs iespējama, ja ir pieprasītas oficiālas recenzijas izmaiņas, pat ja ir pietiekošs apstiprinājumu skaits.", "repo.settings.block_outdated_branch": "Bloķēt sapludināšanau, ja izmaiņu pieprasījums ir novecojis", "repo.settings.block_outdated_branch_desc": "Sapludināšana nebūs pieejama, ja atzars būs atpalicis no bāzes atzara.", - "repo.settings.default_branch_desc": "Norādiet noklusēto repozitorija atzaru izmaiņu pieprasījumiem un koda revīzijām:", "repo.settings.merge_style_desc": "Sapludināšanas veidi", "repo.settings.choose_branch": "Izvēlieties atzaru…", "repo.settings.no_protected_branch": "Nav neviena aizsargātā atzara.", @@ -2071,8 +2043,6 @@ "repo.diff.too_many_files": "Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels", "repo.diff.show_more": "Rādīt vairāk", "repo.diff.load": "Ielādēt izmaiņas", - "repo.diff.generated": "ģenerēts", - "repo.diff.vendored": "ārējs", "repo.diff.comment.add_line_comment": "Pievienot rindas komentāru", "repo.diff.comment.placeholder": "Ievadiet komentāru", "repo.diff.comment.add_single_comment": "Pievienot vienu komentāru", @@ -2088,6 +2058,7 @@ "repo.diff.review.reject": "Pieprasīt izmaiņas", "repo.diff.review.self_approve": "Izmaiņu pieprasījuma autors nevar apstiprināt savu izmaiņu pieprasījumu", "repo.diff.committed_by": "revīziju iesūtīja", + "repo.commits.avatar_stack_and": "un", "repo.diff.protected": "Aizsargāts", "repo.diff.image.side_by_side": "Blakus", "repo.diff.image.swipe": "Pārvelkot", @@ -2161,7 +2132,6 @@ "repo.branch.restore_success": "Tika atjaunots atzars \"%s\".", "repo.branch.restore_failed": "Neizdevās atjaunot atzaru \"%s\".", "repo.branch.protected_deletion_failed": "Atzars \"%s\" ir aizsargāts. To nevar dzēst.", - "repo.branch.default_deletion_failed": "Atzars \"%s\" ir noklusētais atzars un to nevar dzēst.", "repo.branch.restore": "Atjaunot atzaru \"%s\"", "repo.branch.download": "Lejupielādēt atzaru \"%s\"", "repo.branch.rename": "Pārsaukt atzaru \"%s\"", @@ -2239,9 +2209,7 @@ "org.settings.labels_desc": "Pievienojiet iezīmes, kas var tikt izmantotas visos šīs organizācijas repozitorijos.", "org.members.membership_visibility": "Dalībnieka redzamība:", "org.members.public": "Redzams", - "org.members.public_helper": "padarīt slēptu", "org.members.private": "Slēpts", - "org.members.private_helper": "padarīt redzemu", "org.members.member_role": "Dalībnieka loma:", "org.members.owner": "Īpašnieks", "org.members.member": "Dalībnieks", @@ -2271,7 +2239,6 @@ "org.teams.members": "Komandas biedri", "org.teams.update_settings": "Saglabāt iestatījumus", "org.teams.delete_team": "Dzēst komandu", - "org.teams.add_team_member": "Pievienot komandas biedru", "org.teams.invite_team_member": "Uzaicināt komandā \"%s\"", "org.teams.invite_team_member.list": "Neapstiprinātie uzaicinājumi", "org.teams.delete_team_title": "Dzēst komandu", @@ -2296,6 +2263,10 @@ "org.teams.all_repositories_read_permission_desc": "Šī komanda piešķirt skatīšanās tiesības visiem repozitorijiem: komandas biedri var skatīties un klonēt visus organizācijas repozitorijus.", "org.teams.all_repositories_write_permission_desc": "Šī komanda piešķirt labošanas tiesības visiem repozitorijiem: komandas biedri var skatīties un nosūtīt izmaiņas visiem organizācijas repozitorijiem.", "org.teams.all_repositories_admin_permission_desc": "Šī komanda piešķirt administratora tiesības visiem repozitorijiem: komandas biedri var skatīties, nosūtīt izmaiņas un mainīt iestatījumus visiem organizācijas repozitorijiem.", + "org.teams.visibility": "Redzamība", + "org.teams.visibility_private": "Privāts", + "org.teams.visibility_limited": "Ierobežota", + "org.teams.visibility_public": "Publiska", "org.teams.invite.title": "Tu esi uzaicināts pievienoties organizācijas %[2]s komandai %[1]s.", "org.teams.invite.by": "Uzaicināja %s", "org.teams.invite.description": "Nospiediet pogu zemāk, lai pievienotos komandai.", @@ -2309,6 +2280,7 @@ "admin.hooks": "Tīmekļa āķi", "admin.integrations": "Integrācijas", "admin.authentication": "Autentificēšanas avoti", + "admin.badges.description": "Apraksts", "admin.config": "Konfigurācija", "admin.config_summary": "Kopsavilkums", "admin.config_settings": "Iestatījumi", @@ -2577,11 +2549,8 @@ "admin.config.server_config": "Servera konfigurācija", "admin.config.app_name": "Vietnes nosaukums", "admin.config.app_ver": "Gitea versija", - "admin.config.app_url": "Gitea pamata URL", "admin.config.custom_conf": "Konfigurācijas faila ceļš", "admin.config.custom_file_root_path": "Pielāgoto failu pamata ceļš", - "admin.config.domain": "Servera domēns", - "admin.config.offline_mode": "Bezsaistes režīms", "admin.config.disable_router_log": "Atspējot maršrutētāja žurnalizēšanu", "admin.config.run_user": "Izpildes lietotājs", "admin.config.run_mode": "Izpildes režīms", @@ -2631,7 +2600,6 @@ "admin.config.default_enable_timetracking": "Pēc noklusējuma iespējot laika uzskaiti", "admin.config.default_allow_only_contributors_to_track_time": "Atļaut tikai dalībniekiem uzskaitīt laiku", "admin.config.no_reply_address": "Neatbildēt e-pasta adreses domēns", - "admin.config.default_visibility_organization": "Noklusētā redzamība jaunām organizācijām", "admin.config.default_enable_dependencies": "Pēc noklusējuma iespējot problēmu atkarības", "admin.config.webhook_config": "Tīkla āķu konfigurācija", "admin.config.queue_length": "Rindas garums", @@ -2659,11 +2627,9 @@ "admin.config.cache_config": "Kešatmiņas konfigurācija", "admin.config.cache_adapter": "Kešatmiņas adapteris", "admin.config.cache_interval": "Kešatmiņas intervāls", - "admin.config.cache_conn": "Kešatmiņas pieslēguma parametri", "admin.config.cache_item_ttl": "Kešatmiņas vienuma TTL", "admin.config.session_config": "Sesijas konfigurācja", "admin.config.session_provider": "Sesijas nodrošinātājs", - "admin.config.provider_config": "Pakalpojumu sniedzēja konfigurācija", "admin.config.cookie_name": "Sīkdatnes nosaukums", "admin.config.gc_interval_time": "GC laika intervāls", "admin.config.session_life_time": "Sesijas ilgums", @@ -2671,7 +2637,6 @@ "admin.config.cookie_life_time": "Sīkdatņu glabāšanas ilgums", "admin.config.picture_config": "Attēlu un profila bilžu konfigurācija", "admin.config.picture_service": "Lokāli attēli", - "admin.config.disable_gravatar": "Atspējot Gravatar", "admin.config.enable_federated_avatar": "Iespējot apvienotās profila bildes", "admin.config.git_config": "Git konfigurācija", "admin.config.git_disable_diff_highlight": "Atspējot salīdzināšanas sintakses iekrāsošanu", @@ -2681,8 +2646,6 @@ "admin.config.git_gc_args": "GC argumenti", "admin.config.git_migrate_timeout": "Migrācijas noilgums", "admin.config.git_mirror_timeout": "Spoguļa atjaunošanas noilgums", - "admin.config.git_clone_timeout": "Klonēšanas darbības noilgums", - "admin.config.git_pull_timeout": "Izmaiņu saņemšanas darbības noilgums", "admin.config.git_gc_timeout": "GC darbības noilgums", "admin.config.log_config": "Žurnalizēšanas konfigurācija", "admin.config.logger_name_fmt": "Žurnalizētājs: %s", @@ -2903,6 +2866,10 @@ "packages.rubygems.required.rubygems": "Nepieciešamā RubyGem versija", "packages.swift.install": "Pievienojiet pakotni savā Package.swift failā:", "packages.swift.install2": "un izpildiet sekojošu komandu:", + "packages.terraform.install2": "un izpildiet sekojošu komandu:", + "packages.terraform.lock_status": "Slēgšanas maiņu", + "packages.terraform.lock": "Slēgt", + "packages.terraform.unlock": "Atļaut", "packages.vagrant.install": "Lai pievienotu Vagrant kasti, izpildiet sekojošu komandu:", "packages.settings.link": "Piesaistīt pakotni šim repozitorijam", "packages.settings.link.select": "Norādiet repozitoriju", @@ -3010,15 +2977,21 @@ "actions.runs.status": "Statuss", "actions.runs.actors_no_select": "Visi aktori", "actions.runs.status_no_select": "Visi stāvokļi", + "actions.runs.branch": "Atzara izmaiņas", + "actions.runs.branches_no_select": "Visi atzari", "actions.runs.no_results": "Netika atrasts nekas atbilstošs.", "actions.runs.no_workflows": "Vēl nav nevienas darbplūsmas.", "actions.runs.no_runs": "Darbplūsmai vēl nav nevienas izpildes.", "actions.runs.empty_commit_message": "(tukšs revīzijas ziņojums)", + "actions.runs.summary": "Kopsavilkums", "actions.workflow.disable": "Atspējot darbplūsmu", "actions.workflow.disable_success": "Darbplūsma '%s' ir veiksmīgi atspējota.", "actions.workflow.enable": "Iespējot darbplūsmu", "actions.workflow.enable_success": "Darbplūsma '%s' ir veiksmīgi iespējota.", "actions.workflow.disabled": "Darbplūsma ir atspējota.", + "actions.workflow.scope_owner": "Īpašnieks", + "actions.workflow.scope_global": "Globāls", + "actions.workflow.required": "Obligāts", "actions.need_approval_desc": "Nepieciešams apstiprinājums, lai izpildītu izmaiņu pieprasījumu darbaplūsmas no atdalītiem repozitorijiem.", "actions.variables": "Mainīgie", "actions.variables.management": "Mainīgo pārvaldība", @@ -3039,8 +3012,6 @@ "projects.type-2.display_name": "Repozitorija projekts", "projects.type-3.display_name": "Organizācijas projekts", "git.filemode.directory": "Direktorija", - "git.filemode.normal_file": "Parasts fails", - "git.filemode.executable_file": "Izpildāmais fails", - "git.filemode.symbolic_link": "Simboliska saite", - "git.filemode.submodule": "Apakšmodulis" + "git.filemode.submodule": "Apakšmodulis", + "actions.general.token_permissions.mode.restricted": "Ierobežots" } diff --git a/options/locale/locale_nl-NL.json b/options/locale/locale_nl-NL.json index a743877c98..459bbd4286 100644 --- a/options/locale/locale_nl-NL.json +++ b/options/locale/locale_nl-NL.json @@ -146,12 +146,6 @@ "install.register_confirm": "E-mailbevestiging vereist bij registreren", "install.mail_notify": "Activeer e-mailnotificaties", "install.server_service_title": "Server en Third-Party Service-instellingen", - "install.offline_mode": "Lokale modus inschakelen", - "install.offline_mode_popup": "Schakel third-party content uit en gebruik alleen lokale middelen.", - "install.disable_gravatar": "Gravatar uitschakelen", - "install.disable_gravatar_popup": "Gravatar en derden avatar bronnen uitschakelen. Een standaard avatar zal worden gebruikt, tenzij een gebruiker een lokale avatar uploadt.", - "install.federated_avatar_lookup": "Federated Avatars toestaan", - "install.federated_avatar_lookup_popup": "Enable federated avatars lookup to use federated open source service based on libravatar.", "install.disable_registration": "Schakel zelf registratie uit", "install.disable_registration_popup": "Schakel zelfregistratie uit, alleen admins kunnen accounts maken.", "install.allow_only_external_registration_popup": "Registratie alleen via externe diensten toestaan", @@ -170,11 +164,9 @@ "install.admin_email": "E-mailadres", "install.install_btn_confirm": "Installeer Gitea", "install.test_git_failed": "Git test niet gelukt: 'git' commando %v", - "install.sqlite3_not_available": "Deze Gitea-versie biedt geen ondersteuning voor SQLite3. Download de officiële build van %s (niet de versie van de 'gobuild').", "install.invalid_db_setting": "De database instelling zijn niet correct: %v", "install.invalid_repo_path": "Het pad van de hoofdmap van de repository is ongeldig: %v", "install.invalid_app_data_path": "Ongeldig app-gegevenspad: %v", - "install.run_user_not_match": "De 'uitvoeren als' gebruikersnaam is niet de huidige gebruikersnaam: %s -> %s", "install.internal_token_failed": "Interne token genereren mislukt: %v", "install.secret_key_failed": "Geheime sleutel genereren mislukt: %v", "install.save_config_failed": "Kan de configuratie niet opslaan: %v", @@ -543,7 +535,6 @@ "settings.permission_read": "Gelezen", "settings.manage_oauth2_applications": "Beheer OAuth2-applicaties", "settings.edit_oauth2_application": "Bewerk OAuth2-applicatie", - "settings.oauth2_applications_desc": "Met de OAuth2 applicaties kan je derde partij toepassing veilig authenticeren voor gebruikers in deze Gitea.", "settings.remove_oauth2_application": "Verwijder OAuth2 applicatie", "settings.remove_oauth2_application_desc": "Het verwijderen van een OAuth2 app zal de toegang tot alle ondertekende toegangstokens intrekken. Doorgaan?", "settings.remove_oauth2_application_success": "De applicatie is verwijderd.", @@ -556,7 +547,6 @@ "settings.oauth2_regenerate_secret": "Secret opnieuw genereren", "settings.oauth2_regenerate_secret_hint": "Uw secret verloren?", "settings.oauth2_application_edit": "Wijzig", - "settings.oauth2_application_create_description": "OAuth2 applicaties geven je derde partij applicatie toegang tot gebruikersaccounts op deze instantie.", "settings.authorized_oauth2_applications": "Geautoriseerde OAuth2 applicaties", "settings.revoke_key": "Intrekken", "settings.revoke_oauth2_grant": "Toegang intrekken", @@ -607,7 +597,6 @@ "repo.visibility": "Zichtbaarheid", "repo.visibility_description": "Alleen de eigenaar of de organisatielid kan het zien als ze rechten hebben.", "repo.visibility_helper_forced": "De sitebeheerder verplicht alle repositories om privé te zijn.", - "repo.visibility_fork_helper": "(Verandering van deze waarde zal van invloed zijn op alle forks)", "repo.clone_helper": "Heb je hulp nodig om te clonen? Bekijk dan de handleiding.", "repo.fork_repo": "Repository forken", "repo.fork_from": "Afsplitsing van", @@ -821,13 +810,11 @@ "repo.editor.push_rejected": "De wijziging is afgewezen door de server. Controleer Controleer de Git Hooks alsjeblieft.", "repo.editor.push_rejected_summary": "Volledig afwijzingsbericht:", "repo.editor.add_subdir": "Een map toevoegen…", - "repo.editor.no_commit_to_branch": "Kan niet rechtstreeks naar branch committen omdat:", "repo.editor.user_no_push_to_branch": "Gebruiker kan niet pushen naar branch", "repo.editor.require_signed_commit": "Branch vereist een ondertekende commit", "repo.editor.cherry_pick": "Cherry-pick %s op:", "repo.editor.revert": "%s ongedaan maken op:", "repo.commits.desc": "Bekijk de broncode-wijzigingsgeschiedenis.", - "repo.commits.nothing_to_compare": "Deze branches zijn gelijk.", "repo.commits.search_all": "Alle branches", "repo.commits.author": "Auteur", "repo.commits.message": "Bericht", @@ -875,11 +862,9 @@ "repo.issues.filter_reviewers": "Beoordeler filteren", "repo.issues.new": "Nieuw probleem", "repo.issues.new.title_empty": "Titel kan niet leeg zijn", - "repo.issues.new.no_label": "Geen label", "repo.issues.new.clear_labels": "Verwijder labels", "repo.issues.new.projects": "Projecten", "repo.issues.new.clear_projects": "Projecten wissen", - "repo.issues.new.no_projects": "Geen project", "repo.issues.new.open_projects": "Open projecten", "repo.issues.new.closed_projects": "Gesloten Projecten", "repo.issues.new.no_items": "Geen items", @@ -970,7 +955,6 @@ "repo.issues.context.reference_issue": "Verwijs in nieuw issue", "repo.issues.context.edit": "Bewerken", "repo.issues.context.delete": "Verwijder", - "repo.issues.reopen_issue": "Heropen", "repo.issues.create_comment": "Reageer", "repo.issues.closed_at": "heeft dit probleem gesloten %[2]s", "repo.issues.reopened_at": "heropende dit probleem %[2]s", @@ -1123,7 +1107,6 @@ "repo.pulls.allow_edits_from_maintainers": "Bewerkingen toestaan van maintainers", "repo.pulls.allow_edits_from_maintainers_desc": "Gebruikers met schrijftoegang tot de basis branch kunnen ook pushen naar deze branch", "repo.pulls.allow_edits_from_maintainers_err": "Updaten mislukt", - "repo.pulls.compare_changes_desc": "Selecteer de samen te voegen doel- en bron-branch.", "repo.pulls.has_viewed_file": "Gezien", "repo.pulls.has_changed_since_last_review": "Veranderd sinds de laatste beoordeling", "repo.pulls.viewed_files_label": "%[1]d / %[2]d bestanden bekeken", @@ -1131,7 +1114,6 @@ "repo.pulls.compare_compare": "trekken van", "repo.pulls.switch_comparison_type": "Wissel vergelijking type", "repo.pulls.switch_head_and_base": "Verwissel hoofd en basis", - "repo.pulls.nothing_to_compare": "Deze branches zijn gelijk. Er is geen pull-aanvraag nodig.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "Deze branches zijn gelijk. Deze pull verzoek zal leeg zijn.", "repo.pulls.has_pull_request": "Een pull-verzoek tussen deze branches bestaat al: %[2]s#%[3]d", "repo.pulls.create": "Pull verzoek aanmaken", @@ -1185,7 +1167,6 @@ "repo.pulls.status_checking": "Sommige controles zijn in behandeling", "repo.pulls.status_checks_success": "Alle checks waren succesvol", "repo.pulls.status_checks_warning": "Sommige controles hebben waarschuwingen gerapporteerd", - "repo.pulls.status_checks_failure": "Sommige controles zijn mislukt", "repo.pulls.status_checks_error": "Sommige controles hebben foutmeldingen gerapporteerd", "repo.pulls.status_checks_requested": "Vereist", "repo.pulls.update_branch": "Update branch via samenvoegen", @@ -1390,8 +1371,6 @@ "repo.settings.webhook.payload": "Inhoud", "repo.settings.webhook.body": "Inhoud", "repo.settings.githook_edit_desc": "Als haak niet actief is, zal monster inhoud worden gepresenteerd. Verlof inhoud leeg zal deze haak uitschakelen.", - "repo.settings.githook_name": "Haak naam", - "repo.settings.githook_content": "Haak inhoud", "repo.settings.update_githook": "Haak bijwerken", "repo.settings.add_webhook_desc": "Gitea stuurt POST verzoeken met een bepaald inhoudstype naar de doel-URL. Lees meer in de webhooks gids.", "repo.settings.payload_url": "Doel URL", @@ -1486,7 +1465,6 @@ "repo.settings.block_on_official_review_requests_desc": "Samenvoegen is niet mogelijk wanneer het officiële herzieningsverzoeken heeft, ook al zijn er genoeg goedkeuringen.", "repo.settings.block_outdated_branch": "Samenvoegen blokkeren als pull request verouderd is", "repo.settings.block_outdated_branch_desc": "Samenvoegen is niet mogelijk als de hoofd branch achter loop op de basis branch.", - "repo.settings.default_branch_desc": "Selecteer een standaard repository branch voor pull requests en code commits:", "repo.settings.choose_branch": "Kies een branch…", "repo.settings.no_protected_branch": "Er zijn geen beschermde branches.", "repo.settings.edit_protected_branch": "Bewerken", @@ -1564,7 +1542,6 @@ "repo.diff.too_many_files": "Sommige bestanden werden niet getoond omdat er teveel bestanden zijn veranderd in deze diff", "repo.diff.show_more": "Meer weergeven", "repo.diff.load": "Laad Diff", - "repo.diff.generated": "gegenereerd", "repo.diff.comment.placeholder": "Opmerking toevoegen", "repo.diff.comment.add_single_comment": "Één reactie toevoegen", "repo.diff.comment.add_review_comment": "Voeg commentaar toe", @@ -1576,6 +1553,7 @@ "repo.diff.review.approve": "Goedkeuren", "repo.diff.review.reject": "Wijzigingen aanvragen", "repo.diff.committed_by": "gecommit door", + "repo.commits.avatar_stack_and": "en", "repo.diff.protected": "Beveiligd", "repo.diff.image.side_by_side": "Zij aan zij", "repo.diff.image.swipe": "Vegen", @@ -1656,9 +1634,7 @@ "org.settings.labels_desc": "Voeg labels toe die kunnen worden gebruikt bij problemen voor alle repositories in deze organisatie.", "org.members.membership_visibility": "Zichtbaarheid lidmaatschap:", "org.members.public": "Zichtbaar", - "org.members.public_helper": "verborgen maken", "org.members.private": "Verborgen", - "org.members.private_helper": "maak zichtbaar", "org.members.member_role": "Rol van lid:", "org.members.owner": "Eigenaar", "org.members.member": "Lid", @@ -1682,7 +1658,6 @@ "org.teams.members": "Team leden", "org.teams.update_settings": "Instellingen bijwerken", "org.teams.delete_team": "Verwijder team", - "org.teams.add_team_member": "Nieuwe team lid aanmaken", "org.teams.delete_team_title": "Verwijder team", "org.teams.delete_team_desc": "Het verwijderen van een team heeft de toegang tot de repository van de leden. Doorgaan?", "org.teams.delete_team_success": "Het team is verwijderd.", @@ -1703,10 +1678,15 @@ "org.teams.all_repositories": "Alle repositories", "org.teams.all_repositories_helper": "Team heeft toegang tot alle repositories. Door dit te selecteren worden alle bestaande repositories aan het team toegevoegd.", "org.teams.all_repositories_read_permission_desc": "Dit team heeft Lees toegang tot alle repositories: leden kunnen repositories bekijken en klonen.", + "org.teams.visibility": "Zichtbaarheid", + "org.teams.visibility_private": "Prive", + "org.teams.visibility_limited": "Beperkt", + "org.teams.visibility_public": "Publiek", "admin.dashboard": "Overzicht", "admin.users": "Gebruikersacount", "admin.organizations": "Organisaties", "admin.authentication": "Authenticatie bronnen", + "admin.badges.description": "Omschrijving", "admin.config": "Configuratie", "admin.config_summary": "Overzicht", "admin.config_settings": "Instellingen", @@ -1898,11 +1878,8 @@ "admin.config.server_config": "Serverconfiguratie", "admin.config.app_name": "Naam site", "admin.config.app_ver": "Gitea versie", - "admin.config.app_url": "Gitea basis URL", "admin.config.custom_conf": "Configuratiebestandspad", "admin.config.custom_file_root_path": "Hoofdmap voor aangepaste bestanden", - "admin.config.domain": "Server Domein", - "admin.config.offline_mode": "Lokale modus", "admin.config.disable_router_log": "Router-log uitschakelen", "admin.config.run_user": "Uitvoeren als gebruiker", "admin.config.run_mode": "Uitvoer modus", @@ -1947,7 +1924,6 @@ "admin.config.default_enable_timetracking": "Tijdregistratie standaard inschakelen", "admin.config.default_allow_only_contributors_to_track_time": "Sta alleen bijdragers toe tijdregistratie te gebruiken", "admin.config.no_reply_address": "Verborgen e-maildomein", - "admin.config.default_visibility_organization": "Standaard zichtbaarheid voor nieuwe organisaties", "admin.config.default_enable_dependencies": "Issue afhankelijkheden standaard inschakelen", "admin.config.webhook_config": "Webhook configuratie", "admin.config.queue_length": "Lengte van wachtrij", @@ -1967,11 +1943,9 @@ "admin.config.cache_config": "Cache-configuratie", "admin.config.cache_adapter": "Cache-adapter", "admin.config.cache_interval": "Cache-interval", - "admin.config.cache_conn": "Cache-connectie", "admin.config.cache_item_ttl": "Cache-item TTL", "admin.config.session_config": "Sessieconfiguratie", "admin.config.session_provider": "Sessieprovider", - "admin.config.provider_config": "Provider config", "admin.config.cookie_name": "Cookie naam", "admin.config.gc_interval_time": "GC interval time", "admin.config.session_life_time": "Sessie duur", @@ -1979,7 +1953,6 @@ "admin.config.cookie_life_time": "Cookie duur leeftijd", "admin.config.picture_config": "Foto en avatar configuratie", "admin.config.picture_service": "Foto service", - "admin.config.disable_gravatar": "Gravatar uitschakelen", "admin.config.enable_federated_avatar": "Federated avatars toestaan", "admin.config.git_config": "Git configuratie", "admin.config.git_disable_diff_highlight": "Uitschakelen Diff Syntaxis-Highlight", @@ -1988,8 +1961,6 @@ "admin.config.git_gc_args": "GC Parameters", "admin.config.git_migrate_timeout": "Migratie time-out", "admin.config.git_mirror_timeout": "Kopie Update Timeout", - "admin.config.git_clone_timeout": "Kloon operatie timeout", - "admin.config.git_pull_timeout": "Pull operatie timeout", "admin.config.git_gc_timeout": "GC operatie timeout", "admin.config.log_config": "Logconfiguratie", "admin.config.disabled_logger": "Uitgeschakeld", @@ -2085,6 +2056,9 @@ "packages.npm.details.tag": "Label", "packages.rubygems.required.ruby": "Vereist Ruby versie", "packages.rubygems.required.rubygems": "Vereist RubyGem versie", + "packages.terraform.lock_status": "Vergrendel Status", + "packages.terraform.lock": "Vergrendel", + "packages.terraform.unlock": "Ontgrendelen", "packages.settings.link.button": "Repository link bijwerken", "packages.owner.settings.cleanuprules.enabled": "Ingeschakeld", "secrets.creation.description": "Omschrijving", @@ -2093,5 +2067,9 @@ "actions.runners.task_list.run": "Uitvoeren", "actions.runners.task_list.repository": "Opslagplaats", "actions.runners.status.active": "Actief", - "actions.runners.version": "Versie" + "actions.runners.version": "Versie", + "actions.runs.summary": "Overzicht", + "actions.workflow.scope_owner": "Eigenaar", + "actions.workflow.required": "Vereist", + "actions.general.token_permissions.mode.restricted": "Beperkt" } diff --git a/options/locale/locale_pl-PL.json b/options/locale/locale_pl-PL.json index 147febdf09..dec5440d24 100644 --- a/options/locale/locale_pl-PL.json +++ b/options/locale/locale_pl-PL.json @@ -153,12 +153,6 @@ "install.register_confirm": "Wymagają potwierdzenia e-mail przy rejestracji", "install.mail_notify": "Włącz powiadomienia e-mail", "install.server_service_title": "Ustawienia serwera i innych usług", - "install.offline_mode": "Włącz tryb lokalny", - "install.offline_mode_popup": "Wyłącz zewnętrzne usługi dostarczania i dostarczaj wszystkie zasoby lokalnie.", - "install.disable_gravatar": "Wyłącz Gravatar", - "install.disable_gravatar_popup": "Wyłącz Gravatar i inne usługi zewnętrzne awatarów. Zostanie zastosowany domyślny awatar, chyba że użytkownik prześle swój własny.", - "install.federated_avatar_lookup": "Włącz zewnętrzne awatary", - "install.federated_avatar_lookup_popup": "Enable federated avatars lookup to use federated open source service based on libravatar.", "install.disable_registration": "Wyłącz rejestrację dwuskładnikową", "install.disable_registration_popup": "Wyłącz samodzielną rejestrację użytkowników. Tylko administratorzy będą w stanie tworzyć nowe konta.", "install.allow_only_external_registration_popup": "Włącz rejestrację wyłącznie za pomocą zewnętrznych usług", @@ -177,11 +171,9 @@ "install.admin_email": "Adres e-mail", "install.install_btn_confirm": "Zainstaluj Gitea", "install.test_git_failed": "Nie udało się przetestować polecenia „git”: %v", - "install.sqlite3_not_available": "Twoje wydanie Gitea nie obsługuje SQLite3. Pobierz oficjalne wydanie z %s (NIE wersję \"gobuild\").", "install.invalid_db_setting": "Nieprawidłowe ustawienia bazy danych: %v", "install.invalid_repo_path": "Ścieżka repozytorium nie jest poprawna: %v", "install.invalid_app_data_path": "Ścieżka danych aplikacji jest nieprawidłowa: %v", - "install.run_user_not_match": "Użytkownik \"uruchom jako\" nie jest obecnym użytkownikiem: %s -> %s", "install.internal_token_failed": "Nie udało się wygenerować tokenu wewnętrznego: %v", "install.secret_key_failed": "Nie udało się wygenerować tajnego klucza: %v", "install.save_config_failed": "Nie udało się zapisać konfiguracji: %v", @@ -530,7 +522,6 @@ "settings.permission_read": "Przeczytane", "settings.manage_oauth2_applications": "Zarządzaj aplikacjami OAuth2", "settings.edit_oauth2_application": "Edytuj aplikację OAuth2", - "settings.oauth2_applications_desc": "Aplikacje OAuth2 pozwalają Twojej aplikacji zewnętrznej na bezpiecznie uwierzytelnianie użytkowników w tej instancji Gitea.", "settings.remove_oauth2_application": "Usuń aplikację OAuth2", "settings.remove_oauth2_application_desc": "Usuwając aplikację OAuth2 odwołasz jej dostęp do wszystkich podpisanych tokenów dostępu. Kontynuować?", "settings.remove_oauth2_application_success": "Aplikacja została usunięta.", @@ -543,7 +534,6 @@ "settings.oauth2_regenerate_secret": "Ponownie wygeneruj sekretny klucz", "settings.oauth2_regenerate_secret_hint": "Utraciłeś sekretny klucz?", "settings.oauth2_application_edit": "Zmień", - "settings.oauth2_application_create_description": "Aplikacje OAuth2 umożliwiają Twojej aplikacji dostęp do kont użytkowników na tej instancji.", "settings.authorized_oauth2_applications": "Autoryzowane aplikacje OAuth2", "settings.revoke_key": "Odwołaj", "settings.revoke_oauth2_grant": "Odwołaj dostęp", @@ -592,7 +582,6 @@ "repo.visibility": "Widoczność", "repo.visibility_description": "Tylko właściciel lub członkowie organizacji, jeśli mają odpowiednie uprawnienia, będą mogli to zobaczyć.", "repo.visibility_helper_forced": "Administrator strony wymaga, aby nowe repozytoria były prywatne.", - "repo.visibility_fork_helper": "(Zmiana tej wartości wpłynie na wszystkie forki.)", "repo.clone_helper": "Potrzebujesz pomocy z klonowaniem? Odwiedź pomoc.", "repo.fork_repo": "Forkuj repozytorium", "repo.fork_from": "Forkuj z", @@ -792,7 +781,6 @@ "repo.editor.no_changes_to_show": "Brak zmian do pokazania.", "repo.editor.push_rejected_summary": "Pełny komunikat odrzucenia:", "repo.editor.add_subdir": "Dodaj katalog…", - "repo.editor.no_commit_to_branch": "Zatwierdzanie bezpośrednio do tej gałęzi nie jest możliwe, ponieważ:", "repo.editor.user_no_push_to_branch": "Użytkownik nie może wypychać do gałęzi", "repo.editor.require_signed_commit": "Gałąź wymaga podpisanych commitów", "repo.commits.desc": "Przeglądaj historię zmian kodu źródłowego.", @@ -839,11 +827,9 @@ "repo.issues.new": "Nowe zgłoszenie", "repo.issues.new.title_empty": "Tytuł nie może być pusty", "repo.issues.new.labels": "Etykiety", - "repo.issues.new.no_label": "Brak etykiety", "repo.issues.new.clear_labels": "Wyczyść etykiety", "repo.issues.new.projects": "Projekty", "repo.issues.new.clear_projects": "Wyczyść projekty", - "repo.issues.new.no_projects": "Brak projektu", "repo.issues.new.open_projects": "Otwórz projekty", "repo.issues.new.closed_projects": "Zamknięte projekty", "repo.issues.new.no_items": "Brak elementów", @@ -930,7 +916,6 @@ "repo.issues.context.quote_reply": "Cytuj odpowiedź", "repo.issues.context.edit": "Edytuj", "repo.issues.context.delete": "Usuń", - "repo.issues.reopen_issue": "Otwórz ponownie", "repo.issues.create_comment": "Skomentuj", "repo.issues.closed_at": "zamknął(-ęła) to zgłoszenie %[2]s", "repo.issues.reopened_at": "otworzył(-a) ponownie to zgłoszenie %[2]s", @@ -1064,11 +1049,9 @@ "repo.pulls.desc": "Włącz Pull Requesty i recenzjonowanie kodu.", "repo.pulls.new": "Nowy Pull Request", "repo.pulls.compare_changes": "Nowy Pull Request", - "repo.pulls.compare_changes_desc": "Wybierz gałąź, do której chcesz scalić oraz gałąź, z której pobrać zmiany.", "repo.pulls.compare_base": "scal do", "repo.pulls.compare_compare": "ściągnij z", "repo.pulls.filter_branch": "Filtruj branch", - "repo.pulls.nothing_to_compare": "Te gałęzie są sobie równe. Nie ma potrzeby tworzyć Pull Requesta.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "Te gałęzie są równe. Ten PR będzie pusty.", "repo.pulls.create": "Utwórz Pull Request", "repo.pulls.title_desc": "chce scalić %[1]d commity/ów z %[2]s do %[3]s", @@ -1116,7 +1099,6 @@ "repo.pulls.status_checking": "Niektóre etapy są w toku", "repo.pulls.status_checks_success": "Wszystkie etapy powiodły się", "repo.pulls.status_checks_warning": "Niektóre etapy zgłosiły ostrzeżenia", - "repo.pulls.status_checks_failure": "Niektóre etapy nie powiodły się", "repo.pulls.status_checks_error": "Niektóre etapy zgłosiły błędy", "repo.pulls.status_checks_requested": "Wymagane", "repo.pulls.status_checks_details": "Szczegóły", @@ -1296,7 +1278,6 @@ "repo.settings.transfer_abort": "Anuluj transfer", "repo.settings.transfer_abort_invalid": "Nie możesz anulować nieistniejącego transferu repozytorium.", "repo.settings.transfer_desc": "Przenieś to repozytorium do innego użytkownika lub organizacji, w której posiadasz uprawnienia administratora.", - "repo.settings.transfer_form_title": "Wpisz nazwę repozytorium w celu potwierdzenia:", "repo.settings.transfer_notices_1": "- Stracisz dostęp do tego repozytorium, jeśli przeniesiesz je do innego użytkownika.", "repo.settings.transfer_notices_2": "- Utrzymasz dostęp do tego repozytorium, jeśli przeniesiesz je do organizacji, której jesteś (współ-)właścicielem.", "repo.settings.transfer_notices_3": "- Jeśli repozytorium jest prywatne i jest przenoszone do indywidualnego użytkownika, ta czynność upewnia się, że użytkownik ma co najmniej uprawnienia do odczytu (i w razie potrzeby zmienia uprawnienia).", @@ -1350,8 +1331,6 @@ "repo.settings.webhook.payload": "Zawartość", "repo.settings.webhook.body": "Treść", "repo.settings.githook_edit_desc": "Jeśli hook jest nieaktywny, zaprezentowana zostanie przykładowa treść. Pozostawienie pustej wartości wyłączy ten hook.", - "repo.settings.githook_name": "Nazwa hooka", - "repo.settings.githook_content": "Treść hooka", "repo.settings.update_githook": "Zaktualizuj hook", "repo.settings.add_webhook_desc": "Gitea wyśle żądanie POST z określonym typem zawartości do docelowego adresu URL. Przeczytaj o tym więcej w przewodniku o Webhookach.", "repo.settings.payload_url": "Adres docelowy URL", @@ -1447,7 +1426,6 @@ "repo.settings.block_on_official_review_requests_desc": "Połączenie nie będzie możliwe, gdy będzie miało oficjalne wnioski o dokonanie przeglądu, nawet jeśli jest wystarczająco dużo zezwoleń.", "repo.settings.block_outdated_branch": "Zablokuj scalanie, jeśli pull request jest nieaktualny", "repo.settings.block_outdated_branch_desc": "Scalanie nie będzie możliwe, gdy gałąź główna jest za gałęzią bazową.", - "repo.settings.default_branch_desc": "Wybierz domyślną gałąź repozytorium dla Pull Requestów i commitów kodu:", "repo.settings.choose_branch": "Wybierz gałąź…", "repo.settings.no_protected_branch": "Nie ma chronionych gałęzi.", "repo.settings.edit_protected_branch": "Zmień", @@ -1521,7 +1499,6 @@ "repo.diff.file_image_height": "Wysokość", "repo.diff.file_byte_size": "Rozmiar", "repo.diff.file_suppressed": "Plik diff jest za duży", - "repo.diff.generated": "wygenerowano", "repo.diff.comment.placeholder": "Zostaw komentarz", "repo.diff.comment.add_single_comment": "Dodaj jeden komentarz", "repo.diff.comment.add_review_comment": "Dodaj komentarz", @@ -1534,6 +1511,7 @@ "repo.diff.review.approve": "Zatwierdź", "repo.diff.review.reject": "Zażądaj zmian", "repo.diff.committed_by": "zatwierdzone przez", + "repo.commits.avatar_stack_and": "i", "repo.diff.protected": "Chronione", "repo.diff.image.overlay": "Nakładka", "repo.releases.desc": "Śledź wersje projektu i pobrania.", @@ -1636,9 +1614,7 @@ "org.settings.labels_desc": "Dodaj etykiety, które mogą być używane w zgłoszeniach dla wszystkich repozytoriów w tej organizacji.", "org.members.membership_visibility": "Widoczność członkostwa:", "org.members.public": "Widoczny", - "org.members.public_helper": "ukryj", "org.members.private": "Ukryty", - "org.members.private_helper": "pokaż", "org.members.member_role": "Rola:", "org.members.owner": "Właściciel", "org.members.member": "Członek", @@ -1664,7 +1640,6 @@ "org.teams.members": "Członkowie zespołu", "org.teams.update_settings": "Aktualizuj ustawienia", "org.teams.delete_team": "Usuń zespół", - "org.teams.add_team_member": "Dodaj członka zespołu", "org.teams.delete_team_title": "Usuń zespół", "org.teams.delete_team_desc": "Usunięcie zespołu wycofa dostęp do repozytorium jego członkom. Kontynuować?", "org.teams.delete_team_success": "Zespół został usunięty.", @@ -1687,12 +1662,17 @@ "org.teams.all_repositories_read_permission_desc": "Ten zespół nadaje uprawnienie Odczytu do wszystkich repozytoriów: jego członkowie mogą wyświetlać i klonować repozytoria.", "org.teams.all_repositories_write_permission_desc": "Ten zespół nadaje uprawnienie Zapisu do wszystkich repozytoriów: jego członkowie mogą odczytywać i przesyłać do repozytoriów.", "org.teams.all_repositories_admin_permission_desc": "Ten zespół nadaje uprawnienia Administratora do wszystkich repozytoriów: jego członkowie mogą odczytywać, przesyłać oraz dodawać innych współtwórców do repozytoriów.", + "org.teams.visibility": "Widoczność", + "org.teams.visibility_private": "Prywatne", + "org.teams.visibility_limited": "Ograniczony", + "org.teams.visibility_public": "Publiczne", "admin.dashboard": "Pulpit", "admin.users": "Konta użytkownika", "admin.organizations": "Organizacje", "admin.repositories": "Repozytoria", "admin.hooks": "Weebhook'i", "admin.authentication": "Źródła uwierzytelniania", + "admin.badges.description": "Opis", "admin.config": "Konfiguracja", "admin.config_summary": "Podsumowanie", "admin.config_settings": "Ustawienia", @@ -1900,11 +1880,8 @@ "admin.config.server_config": "Konfiguracja serwera", "admin.config.app_name": "Tytuł strony", "admin.config.app_ver": "Wersja Gitea", - "admin.config.app_url": "Podstawowy adres URL Gitea", "admin.config.custom_conf": "Ścieżka do pliku konfiguracyjnego", "admin.config.custom_file_root_path": "Ścieżka główna plików niestandardowych", - "admin.config.domain": "Domena serwera", - "admin.config.offline_mode": "Tryb lokalny", "admin.config.disable_router_log": "Wyłącz dziennik routera", "admin.config.run_user": "Uruchom jako nazwa użytkownika", "admin.config.run_mode": "Tryb uruchamienia", @@ -1951,7 +1928,6 @@ "admin.config.default_enable_timetracking": "Domyślnie włącz śledzenie czasu", "admin.config.default_allow_only_contributors_to_track_time": "Zezwalaj wyłącznie współpracownikom na śledzenie czasu", "admin.config.no_reply_address": "Ukryta domena e-mail", - "admin.config.default_visibility_organization": "Domyślna widoczność dla nowych organizacji", "admin.config.default_enable_dependencies": "Domyślne włączanie zależności zgłoszeń", "admin.config.webhook_config": "Konfiguracja webhooka", "admin.config.queue_length": "Długość kolejki", @@ -1971,11 +1947,9 @@ "admin.config.cache_config": "Konfiguracja pamięci podręcznej", "admin.config.cache_adapter": "Adapter pamięci podręcznej", "admin.config.cache_interval": "Interwał pamięci podręcznej", - "admin.config.cache_conn": "Połączenie z pamięcią podręczną", "admin.config.cache_item_ttl": "TTL składnika pamięci podręcznej", "admin.config.session_config": "Konfiguracja sesji", "admin.config.session_provider": "Dostawca sesji", - "admin.config.provider_config": "Konfiguracja dostawcy", "admin.config.cookie_name": "Nazwa ciasteczka", "admin.config.gc_interval_time": "Interwał usuwania śmieci", "admin.config.session_life_time": "Czas ważności sesji", @@ -1983,7 +1957,6 @@ "admin.config.cookie_life_time": "Czas ważności ciasteczka", "admin.config.picture_config": "Konfiguracja obrazu i awataru", "admin.config.picture_service": "Usługa obrazów", - "admin.config.disable_gravatar": "Wyłącz Gravatar", "admin.config.enable_federated_avatar": "Włącz sfederowane awatary", "admin.config.git_config": "Konfiguracja Git", "admin.config.git_disable_diff_highlight": "Wyłączyć wyróżnianie składni diff", @@ -1993,8 +1966,6 @@ "admin.config.git_gc_args": "Argumenty GC", "admin.config.git_migrate_timeout": "Limit czasu migracji", "admin.config.git_mirror_timeout": "Limit czasu aktualizacji kopii lustrzanej", - "admin.config.git_clone_timeout": "Limit czasu operacji klonowania", - "admin.config.git_pull_timeout": "Limit czasu dla operacji pull", "admin.config.git_gc_timeout": "Limit czasu usuwania śmieci", "admin.config.log_config": "Konfiguracja dziennika", "admin.config.disabled_logger": "Wyłączone", @@ -2100,6 +2071,8 @@ "packages.conan.details.repository": "Repozytorium", "packages.container.labels": "Etykiety", "packages.npm.dependencies": "Zależności", + "packages.terraform.lock": "Zablokuj", + "packages.terraform.unlock": "Odblokuj", "packages.owner.settings.cleanuprules.enabled": "Włączone", "secrets.creation.description": "Opis", "actions.runners.name": "Nazwa", @@ -2110,5 +2083,8 @@ "actions.runners.task_list.repository": "Repozytorium", "actions.runners.status.active": "Aktywne", "actions.runners.version": "Wersja", - "git.filemode.symbolic_link": "Dowiązanie symboliczne" + "actions.runs.summary": "Podsumowanie", + "actions.workflow.scope_owner": "Właściciel", + "actions.workflow.required": "Wymagane", + "actions.general.token_permissions.mode.restricted": "Ograniczone" } diff --git a/options/locale/locale_pt-BR.json b/options/locale/locale_pt-BR.json index e3b9fd1417..4e4611a290 100644 --- a/options/locale/locale_pt-BR.json +++ b/options/locale/locale_pt-BR.json @@ -198,7 +198,6 @@ "startpage.lightweight": "Leve e rápido", "startpage.lightweight_desc": "Gitea tem requisitos minimos e pode rodar até em um Raspberry Pi. Economize energia da sua máquina!", "startpage.license": "Código Aberto", - "startpage.license_desc": "Receba %[2]s! Junte-se a nós contribuindo para tornar este projeto ainda melhor. Não seja tímido, seja um colaborador!", "install.install": "Instalação", "install.title": "Configuração Inicial", "install.docker_helper": "Se você está rodando o Gitea dentro do Docker, por favor leia a documentação cuidadosamente antes de alterar qualquer coisa nesta página.", @@ -233,7 +232,6 @@ "install.lfs_path": "Caminho raiz do Git LFS", "install.lfs_path_helper": "Os arquivos armazenados com o Git LFS serão armazenados neste diretório. Deixe em branco para desabilitar.", "install.run_user": "Executar como Usuário", - "install.run_user_helper": "O nome de usuário do sistema operacional com o qual o Gitea é executado. Observe que este usuário deve ter acesso ao caminho da raiz do repositório.", "install.domain": "Domínio do Servidor", "install.domain_helper": "Domínio ou endereço de host para o servidor.", "install.ssh_port": "Porta do servidor SSH", @@ -256,12 +254,6 @@ "install.register_confirm": "Exigir Confirmação de E-mail para se Cadastrar", "install.mail_notify": "Habilitar Notificações de E-mail", "install.server_service_title": "Configurações de Servidor e Serviços de Terceiros", - "install.offline_mode": "Habilitar Autenticação Local", - "install.offline_mode_popup": "Desabilitar redes de entrega de conteúdo de terceiros e entregar todos os recursos localmente.", - "install.disable_gravatar": "Desabilitar o Gravatar", - "install.disable_gravatar_popup": "Desabilitar o gravatar e avatar de fontes de terceiros. Um avatar padrão será usado a menos que um usuário localmente carrega um avatar.", - "install.federated_avatar_lookup": "Habilitar Avatares Federativos", - "install.federated_avatar_lookup_popup": "Habilitar a busca federativa de avatares a usar o serviço federativo de código aberto baseado no libravatar.", "install.disable_registration": "Desabilitar Auto-Cadastro", "install.disable_registration_popup": "Desabilitar auto-cadastro de usuário. Somente os administradores serão capazes de criar novas contas de usuário.", "install.allow_only_external_registration_popup": "Permitir Cadastro Somente por Meio de Serviços Externos", @@ -281,12 +273,10 @@ "install.admin_email": "Endereço de E-mail", "install.install_btn_confirm": "Instalar Gitea", "install.test_git_failed": "Falha ao testar o comando 'git': %v", - "install.sqlite3_not_available": "Esta versão do Gitea não suporta SQLite3. Por favor faça o download da versão binária oficial em %s (não utilize a versão 'gobuild').", "install.invalid_db_setting": "Configuração de banco de dados está inválida: %v", "install.invalid_db_table": "A tabela \"%s\" do banco de dados é inválida: %v", "install.invalid_repo_path": "A raiz do repositório está inválida: %v", "install.invalid_app_data_path": "O caminho dos dados do aplicativo é inválido: %v", - "install.run_user_not_match": "O nome de usuário 'Executar como' não é o nome de usuário atual: %s -> %s", "install.internal_token_failed": "Falha ao gerar o token interno: %v", "install.secret_key_failed": "Falha ao gerar a chave secreta: %v", "install.save_config_failed": "Falha ao salvar a configuração: %v", @@ -591,7 +581,6 @@ "user.block.unblock.failure": "Falha ao desbloquear o usuário: %s", "user.block.blocked": "Você bloqueou este usuário.", "user.block.title": "Bloquear um usuário", - "user.block.info_2": "seguindo sua conta", "user.block.user_to_block": "Usuário a bloquear", "user.block.note": "Nota", "user.block.note.title": "Nota opcional:", @@ -807,7 +796,6 @@ "settings.permissions_list": "Permissões:", "settings.manage_oauth2_applications": "Gerenciar aplicativos OAuth2", "settings.edit_oauth2_application": "Editar aplicativo OAuth2", - "settings.oauth2_applications_desc": "Aplicativos OAuth2 permitem que seus aplicativos de terceiros autentiquem com segurança usuários nesta instância do Gitea.", "settings.remove_oauth2_application": "Remover aplicativo OAuth2", "settings.remove_oauth2_application_desc": "Remover um aplicativo OAuth2 irá revogar o acesso a todos os tokens de acesso assinados. Continuar?", "settings.remove_oauth2_application_success": "O aplicativo foi excluído.", @@ -823,7 +811,6 @@ "settings.oauth2_regenerate_secret_hint": "Perdeu seu Client Secret?", "settings.oauth2_client_secret_hint": "O segredo não será exibido novamente depois que você sair ou atualizar esta página. Certifique-se de que você o salvou.", "settings.oauth2_application_edit": "Editar", - "settings.oauth2_application_create_description": "Os aplicativos OAuth2 fornecem ao seus aplicativos de terceiros acesso a contas de usuário nesta instância.", "settings.oauth2_application_remove_description": "A remoção de um aplicativo OAuth2 impedirá que ele acesse contas de usuários autorizados nesta instância. Continuar?", "settings.oauth2_application_locked": "O Gitea pré-registra alguns aplicativos OAuth2 na inicialização, se habilitados na configuração. Para evitar um comportamento inesperado, eles não podem ser editados nem removidos. Consulte a documentação do OAuth2 para obter mais informações.", "settings.authorized_oauth2_applications": "Aplicações OAuth2 autorizadas", @@ -899,7 +886,6 @@ "repo.visibility_description": "Somente o proprietário ou os membros da organização, se tiverem direitos, poderão vê-lo.", "repo.visibility_helper": "Tornar o repositório privado", "repo.visibility_helper_forced": "O administrador do site força novos repositórios a serem privados.", - "repo.visibility_fork_helper": "(Esta alteração irá afetar todos os forks.)", "repo.clone_helper": "Precisa de ajuda com o clone? Visite a Ajuda.", "repo.fork_repo": "Fork do Repositório", "repo.fork_from": "Fork de", @@ -985,8 +971,6 @@ "repo.transfer.accept_desc": "Transferir para \"%s\"", "repo.transfer.reject": "Rejeitar Transferência", "repo.transfer.reject_desc": "Cancelar a transferência para \"%s\"", - "repo.transfer.no_permission_to_accept": "Você não tem permissão para aceitar essa transferência.", - "repo.transfer.no_permission_to_reject": "Você não tem permissão para rejeitar essa transferência.", "repo.desc.private": "Privado", "repo.desc.public": "Público", "repo.desc.public_access": "Acesso Público", @@ -1117,7 +1101,6 @@ "repo.ambiguous_runes_description": "Este arquivo contém caracteres Unicode que podem ser confundidos com outros caracteres. Se você acha que isso é intencional, pode ignorar esse aviso com segurança. Use o botão Escapar para revelá-los", "repo.invisible_runes_line": "Esta linha tem caracteres unicode invisíveis", "repo.ambiguous_runes_line": "Esta linha tem caracteres unicode ambíguos", - "repo.ambiguous_character": "%[1]c [U+%04[1]X] pode ser confundido com %[2]c [U+%04[2]X]", "repo.escape_control_characters": "Escapar", "repo.unescape_control_characters": "Desescapar", "repo.file_copy_permalink": "Copiar Link Permanente", @@ -1201,7 +1184,6 @@ "repo.editor.upload_file_is_locked": "Arquivo \"%s\" está bloqueado por %s.", "repo.editor.upload_files_to_dir": "Enviar arquivos para \"%s\"", "repo.editor.cannot_commit_to_protected_branch": "Não foi possível enviar commits para o branch protegido \"%s\".", - "repo.editor.no_commit_to_branch": "Não foi possível fazer commit diretamente no branch porque:", "repo.editor.user_no_push_to_branch": "O usuário não pode fazer push no branch", "repo.editor.require_signed_commit": "Branch requer um commit assinado", "repo.editor.cherry_pick": "Cherry-pick %s para:", @@ -1210,7 +1192,6 @@ "repo.editor.failed_to_commit_summary": "Mensagem de Erro:", "repo.commits.desc": "Veja o histórico de alterações do código-fonte.", "repo.commits.no_commits": "Nenhum commit em comum. \"%s\" e \"%s\" tem históricos completamente diferentes.", - "repo.commits.nothing_to_compare": "Estes branches são iguais.", "repo.commits.search.tooltip": "Você pode prefixar as palavras-chave com \"author:\" (autor da mudança), \"committer:\" (autor do commit), \"after:\" (depois) ou \"before:\" (antes). Por exemplo: \"revert author:Ana before:2019-01-13\".\\", "repo.commits.search_branch": "Este Branch", "repo.commits.search_all": "Todos os Branches", @@ -1283,11 +1264,9 @@ "repo.issues.new": "Nova Issue", "repo.issues.new.title_empty": "Título não pode ser em branco", "repo.issues.new.labels": "Etiquetas", - "repo.issues.new.no_label": "Sem Etiqueta", "repo.issues.new.clear_labels": "Limpar etiquetas", "repo.issues.new.projects": "Projetos", "repo.issues.new.clear_projects": "Limpar projetos", - "repo.issues.new.no_projects": "Sem projeto", "repo.issues.new.open_projects": "Abrir Projetos", "repo.issues.new.closed_projects": "Projetos Fechados", "repo.issues.new.no_items": "Nenhum item", @@ -1406,7 +1385,6 @@ "repo.issues.comment_pull_merged_at": "aplicou o merge do commit %[1]s em %[2]s %[3]s", "repo.issues.comment_manually_pull_merged_at": "aplicou o merge manual do commit %[1]s em %[2]s %[3]s", "repo.issues.close_comment_issue": "Comentar e Fechar", - "repo.issues.reopen_issue": "Reabrir", "repo.issues.reopen_comment_issue": "Comentar e Reabrir", "repo.issues.create_comment": "Comentar", "repo.issues.closed_at": "fechou esta issue %[2]s", @@ -1609,7 +1587,6 @@ "repo.pulls.allow_edits_from_maintainers": "Permitir edições de mantenedores", "repo.pulls.allow_edits_from_maintainers_desc": "Usuários com acesso de gravação para o branch base também podem fazer push para este branch", "repo.pulls.allow_edits_from_maintainers_err": "Falha na atualização", - "repo.pulls.compare_changes_desc": "Selecione o branch de destino (push) e o branch de origem (pull) para o merge.", "repo.pulls.has_viewed_file": "Visto", "repo.pulls.has_changed_since_last_review": "Alterado desde a última revisão", "repo.pulls.viewed_files_label": "%[1]d / %[2]d arquivos visualizados", @@ -1627,8 +1604,6 @@ "repo.pulls.select_commit_hold_shift_for_range": "Selecionar commit. Mantenha pressionado shift + clique para selecionar um intervalo", "repo.pulls.review_only_possible_for_full_diff": "A revisão só é possível ao visualizar todas as diferenças", "repo.pulls.filter_changes_by_commit": "Filtrar por commit", - "repo.pulls.nothing_to_compare": "Estes branches são iguais. Não há nenhuma necessidade para criar um pull request.", - "repo.pulls.nothing_to_compare_have_tag": "Os branches/tags selecionados são iguais.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "Estes branches são iguais. Este PR ficará vazio.", "repo.pulls.has_pull_request": "Um pull request entre esses branches já existe: %[2]s#%[3]d", "repo.pulls.create": "Criar Pull Request", @@ -1705,7 +1680,6 @@ "repo.pulls.status_checking": "Algumas verificações estão pendentes", "repo.pulls.status_checks_success": "Todas as verificações foram bem sucedidas", "repo.pulls.status_checks_warning": "Algumas verificações reportaram avisos", - "repo.pulls.status_checks_failure": "Algumas verificações falharam", "repo.pulls.status_checks_error": "Algumas verificações reportaram erros", "repo.pulls.status_checks_requested": "Obrigatário", "repo.pulls.status_checks_details": "Detalhes", @@ -1781,7 +1755,6 @@ "repo.signing.wont_sign.headsigned": "O merge não será assinado, pois o commit mais recente não está assinado.", "repo.signing.wont_sign.commitssigned": "O merge não será assinado, pois todos os commits associados não estão assinados.", "repo.signing.wont_sign.approved": "O merge não será assinado porque o PR não foi aprovado.", - "repo.signing.wont_sign.not_signed_in": "Você não está conectado.", "repo.ext_wiki": "Acesso a Wiki Externo", "repo.ext_wiki.desc": "Link para uma wiki externa.", "repo.wiki.welcome": "Bem-vindo a Wiki.", @@ -1987,7 +1960,6 @@ "repo.settings.transfer_abort_invalid": "Não é possível cancelar uma transferência de repositório não existente.", "repo.settings.transfer_abort_success": "A transferência do repositório para %s foi cancelada com sucesso.", "repo.settings.transfer_desc": "Transferir este repositório para outro usuário ou para uma organização onde você tem direitos de administrador.", - "repo.settings.transfer_form_title": "Digite o nome do repositório para confirmar:", "repo.settings.transfer_in_progress": "Há uma transferência em andamento. Por favor, cancele se você gostaria de transferir este repositório para outro usuário.", "repo.settings.transfer_notices_1": "- Você perderá o acesso ao repositório se transferir para um usuário individual.", "repo.settings.transfer_notices_2": "- Você manterá acesso ao repositório se transferi-lo para uma organização que você também é proprietário.", @@ -2004,10 +1976,8 @@ "repo.settings.trust_model.collaborator.long": "Colaborador: Confiar em assinaturas feitas por colaboradores", "repo.settings.trust_model.collaborator.desc": "Assinaturas válidas dos colaboradores deste repositório serão marcadas como \"confiáveis\" - (quer correspondam ao autor do commit ou não). Caso contrário, assinaturas válidas serão marcadas como \"não confiáveis\" se a assinatura corresponder ao autor do commit e \"não corresponde\" se não corresponder.", "repo.settings.trust_model.committer.long": "Committer: Confiar nas assinaturas que correspondam aos autores de commits (isso corresponde ao GitHub e forçará commits assinados pelo Gitea a ter o Gitea como o autor do commit).", - "repo.settings.trust_model.committer.desc": "As assinaturas válidas só serão marcadas como \"confiáveis\" se corresponderem ao autor do commit; caso contrário, serão marcadas como \"não correspondentes\". Isso força o Gitea a ser o autor do commit em commits assinados com o autor do commit real marcado como Co-authored-by: e Co-committed-by: no rodapé do commit. A chave padrão do Gitea deve corresponder a um usuário no banco de dados.", "repo.settings.trust_model.collaboratorcommitter": "Colaborador+Commiter", "repo.settings.trust_model.collaboratorcommitter.long": "Colaborador+Committer: Confiar na assinatura dos colaboradores que correspondem ao autor do commit", - "repo.settings.trust_model.collaboratorcommitter.desc": "Assinaturas válidas dos colaboradores deste repositório serão marcadas como \"confiáveis\" se corresponderem ao autor do commit. Caso contrário, as assinaturas válidas serão marcadas como \"não confiáveis\" se a assinatura corresponder ao autor do commit e \"não corresponde\" caso contrário. Isso forçará o Gitea a ser marcado como o autor do commit nos commits assinados com o autor marcado como Co-Authored-By: e o Committed-By: resumo do commit. A chave padrão do Gitea tem que corresponder a um usuário no banco de dados.", "repo.settings.wiki_delete": "Excluir Dados da Wiki", "repo.settings.wiki_delete_desc": "A exclusão de dados da wiki é permanente e não pode ser desfeita.", "repo.settings.wiki_delete_notices_1": "- Isso excluirá e desabilitará permanentemente a wiki do repositório %s.", @@ -2049,7 +2019,6 @@ "repo.settings.webhook_deletion_success": "O webhook foi removido.", "repo.settings.webhook.test_delivery": "Testar Evento de Push", "repo.settings.webhook.test_delivery_desc": "Teste este webhook com um evento falso.", - "repo.settings.webhook.test_delivery_desc_disabled": "Para testar este webhook com um evento falso, ative-o.", "repo.settings.webhook.request": "Solicitação", "repo.settings.webhook.response": "Resposta", "repo.settings.webhook.headers": "Cabeçalhos", @@ -2060,8 +2029,6 @@ "repo.settings.webhook.delivery.success": "Um evento foi adicionado à fila de envio. Pode levar alguns segundos até que ele apareça no histórico de envio.", "repo.settings.githooks_desc": "Hooks do Git são executados pelo próprio Git. Você pode editar arquivos de hook abaixo para configurar operações personalizadas.", "repo.settings.githook_edit_desc": "Se o hook não estiver ativo, o conteúdo de exemplo será apresentado. Deixar o conteúdo em branco irá desabilitar esse hook.", - "repo.settings.githook_name": "Nome do Hook", - "repo.settings.githook_content": "Conteúdo do Hook", "repo.settings.update_githook": "Atualizar Hook", "repo.settings.add_webhook_desc": "Gitea enviará requisições POST com um tipo de conteúdo especificado para a URL de destino. Leia mais no guia de webhooks.", "repo.settings.payload_url": "URL de Destino", @@ -2188,7 +2155,6 @@ "repo.settings.block_on_official_review_requests_desc": "O merge não será possível quando tiver pedidos de revisão oficiais, mesmo que haja aprovações suficientes.", "repo.settings.block_outdated_branch": "Bloquear o merge se o pull request estiver desatualizado", "repo.settings.block_outdated_branch_desc": "O merge não será possível quando o branch de topo estiver atrás do branch base.", - "repo.settings.default_branch_desc": "Selecione um branch padrão para pull requests e commits de código:", "repo.settings.merge_style_desc": "Estilos de Merge", "repo.settings.choose_branch": "Escolha um branch...", "repo.settings.no_protected_branch": "Não há branches protegidos.", @@ -2278,8 +2244,8 @@ "repo.diff.too_many_files": "Alguns arquivos não foram exibidos porque demasiados arquivos foram alterados neste diff", "repo.diff.show_more": "Mostrar Mais", "repo.diff.load": "Carregar Diff", - "repo.diff.generated": "gerado", - "repo.diff.vendored": "externo", + "repo.diff.generated": "Gerado", + "repo.diff.vendored": "Externo", "repo.diff.comment.add_line_comment": "Adicionar comentário na linha", "repo.diff.comment.placeholder": "Deixe um comentário", "repo.diff.comment.add_single_comment": "Adicionar um único comentário", @@ -2295,6 +2261,7 @@ "repo.diff.review.reject": "Solicitar alterações", "repo.diff.review.self_approve": "Os autores do pull request não podem aprovar seu próprio pull request", "repo.diff.committed_by": "commit de", + "repo.commits.avatar_stack_and": "e", "repo.diff.protected": "Protegido", "repo.diff.image.side_by_side": "Lado a Lado", "repo.diff.image.swipe": "Deslizar", @@ -2361,7 +2328,6 @@ "repo.branch.restore_success": "Branch \"%s\" restaurado.", "repo.branch.restore_failed": "Ocorreu um erro ao restaurar o branch \"%s\".", "repo.branch.protected_deletion_failed": "Branch \"%s\" é protegido. Ele não pode ser excluído.", - "repo.branch.default_deletion_failed": "Branch \"%s\" é o branch padrão. Ele não pode ser excluído.", "repo.branch.default_branch_not_exist": "Branch padrão \"%s\" não existe.", "repo.branch.restore": "Restaurar Branch \"%s\"", "repo.branch.download": "Baixar Branch \"%s\"", @@ -2448,9 +2414,7 @@ "org.settings.labels_desc": "Adicionar rótulos que possam ser usadas em issues para todos os repositórios desta organização.", "org.members.membership_visibility": "Visibilidade da Associação:", "org.members.public": "Público", - "org.members.public_helper": "tornar privado", "org.members.private": "Privado", - "org.members.private_helper": "tornar público", "org.members.member_role": "Categoria de Membro:", "org.members.owner": "Proprietário", "org.members.member": "Membro", @@ -2480,7 +2444,6 @@ "org.teams.members": "Membros da Equipe", "org.teams.update_settings": "Atualizar Configurações", "org.teams.delete_team": "Excluir Equipe", - "org.teams.add_team_member": "Adicionar Membro na Equipe", "org.teams.invite_team_member": "Convidar para %s", "org.teams.invite_team_member.list": "Convites Pendentes", "org.teams.delete_team_title": "Excluir Equipe", @@ -2506,6 +2469,10 @@ "org.teams.all_repositories_read_permission_desc": "Esta equipe concede acesso Leitura a todos os repositórios: membros podem ver e clonar repositórios.", "org.teams.all_repositories_write_permission_desc": "Esta equipe concede acesso Escrita a todos os repositórios: os membros podem ler de e fazer push para os repositórios.", "org.teams.all_repositories_admin_permission_desc": "Esta equipe concede acesso Administrativo a todos os repositórios: os membros podem ler, fazer push e adicionar colaboradores aos repositórios.", + "org.teams.visibility": "Visibilidade", + "org.teams.visibility_private": "Privado", + "org.teams.visibility_limited": "Limitada", + "org.teams.visibility_public": "Pública", "org.teams.invite.title": "Você foi convidado para fazer parte da equipe %s na organização %s.", "org.teams.invite.by": "Convidado por %s", "org.teams.invite.description": "Por favor, clique no botão abaixo para se juntar à equipe.", @@ -2526,6 +2493,7 @@ "admin.repositories": "Repositórios", "admin.integrations": "Integrações", "admin.authentication": "Fontes de Autenticação", + "admin.badges.description": "Descrição", "admin.emails": "E-mails do Usuário", "admin.config": "Configuração", "admin.config_summary": "Resumo", @@ -2805,11 +2773,8 @@ "admin.config.server_config": "Configuração do Servidor", "admin.config.app_name": "Nome do Site", "admin.config.app_ver": "Versão do Gitea", - "admin.config.app_url": "URL base do Gitea", "admin.config.custom_conf": "Caminho do Arquivo de Configuração", "admin.config.custom_file_root_path": "Caminho Raiz para Arquivo Personalizado", - "admin.config.domain": "Domínio do Servidor", - "admin.config.offline_mode": "Modo Local", "admin.config.disable_router_log": "Desabilitar o Log do roteador", "admin.config.run_user": "Executar como Usuário", "admin.config.run_mode": "Modo de Execução", @@ -2858,7 +2823,6 @@ "admin.config.default_enable_timetracking": "Habilitar o Cronômetro por Padrão", "admin.config.default_allow_only_contributors_to_track_time": "Permitir que Apenas os Colaboradores Acompanhem o Cronômetro", "admin.config.no_reply_address": "Ocultar Domínio de E-mail", - "admin.config.default_visibility_organization": "Visibilidade padrão para novas organizações", "admin.config.webhook_config": "Configuração de Hook da Web", "admin.config.queue_length": "Tamanho da Fila", "admin.config.deliver_timeout": "Intervalo de Entrega", @@ -2885,11 +2849,9 @@ "admin.config.cache_config": "Configuração de Cache", "admin.config.cache_adapter": "Adaptador de Cache", "admin.config.cache_interval": "Intervalo de Cache", - "admin.config.cache_conn": "Conexão de Cache", "admin.config.cache_item_ttl": "Item de cache TTL", "admin.config.session_config": "Configuração da Sessão", "admin.config.session_provider": "Provedor da Sessão", - "admin.config.provider_config": "Configuração do Provedor", "admin.config.cookie_name": "Nome do Cookie", "admin.config.gc_interval_time": "Tempo de Intervalo do GC", "admin.config.session_life_time": "Tempo de Vida da Sessão", @@ -2897,7 +2859,6 @@ "admin.config.cookie_life_time": "Tempo de Vida do Cookie", "admin.config.picture_config": "Configuração de Imagem e Avatar", "admin.config.picture_service": "Serviço de Imagens", - "admin.config.disable_gravatar": "Desabilitar o Gravatar", "admin.config.enable_federated_avatar": "Habilitar Avatares Federativos", "admin.config.git_config": "Configuração do Git", "admin.config.git_disable_diff_highlight": "Desabilitar Realce de Mudanças no Diff", @@ -2907,8 +2868,6 @@ "admin.config.git_gc_args": "Argumentos do GC", "admin.config.git_migrate_timeout": "Tempo Limite de Migração", "admin.config.git_mirror_timeout": "Tempo Limite de Atualização de Espelhamento", - "admin.config.git_clone_timeout": "Tempo Limite para Operação de Clone", - "admin.config.git_pull_timeout": "Tempo Limite para Operação de Pull", "admin.config.git_gc_timeout": "Tempo limite para execução do GC", "admin.config.log_config": "Configuração de Log", "admin.config.disabled_logger": "Desabilitado", @@ -3146,6 +3105,10 @@ "packages.swift.registry": "Configure este registro pela linha de comando:", "packages.swift.install": "Adicione o pacote em seu arquivo Package.swift:", "packages.swift.install2": "e execute o seguinte comando:", + "packages.terraform.install2": "e execute o seguinte comando:", + "packages.terraform.lock_status": "Status de Bloqueio", + "packages.terraform.lock": "Bloquear", + "packages.terraform.unlock": "Desbloquear", "packages.vagrant.install": "Para adicionar uma Vagrant box, execute o seguinte comando:", "packages.settings.link": "Vincular este pacote a um repositório", "packages.settings.link.select": "Selecionar Repositório", @@ -3250,17 +3213,21 @@ "actions.runs.actor": "Ator", "actions.runs.actors_no_select": "Todos os atores", "actions.runs.status_no_select": "Todos os Status", + "actions.runs.branches_no_select": "Todos os branches", "actions.runs.no_results": "Não houve correspondência de resultados.", "actions.runs.no_workflows": "Ainda não existem workflows.", "actions.runs.no_workflows.quick_start": "Não sabe como começar com as Ações do Gitea? Veja o guia de início rápido.", "actions.runs.no_workflows.documentation": "Para obter mais informações sobre as Ações do Gitea, consulte a documentação.", "actions.runs.no_runs": "O workflow ainda não foi executado.", "actions.runs.empty_commit_message": "(mensagem de commit vazia)", + "actions.runs.summary": "Resumo", "actions.workflow.disable": "Desabilitar Workflow", "actions.workflow.disable_success": "Workflow '%s' desativado com sucesso.", "actions.workflow.enable": "Habilitar Workflow", "actions.workflow.enable_success": "Workflow '%s' habilitado com sucesso.", "actions.workflow.disabled": "Workflow está desativado.", + "actions.workflow.scope_owner": "Proprietário", + "actions.workflow.required": "Obrigatário", "actions.need_approval_desc": "Precisa de aprovação para executar workflows para pull request do fork.", "actions.variables": "Variáveis", "actions.variables.management": "Gerenciamento de Variáveis", @@ -3283,8 +3250,6 @@ "projects.enter_fullscreen": "Tela cheia", "projects.exit_fullscreen": "Sair da Tela Cheia", "git.filemode.directory": "Diretório", - "git.filemode.normal_file": "Arquivo normal", - "git.filemode.executable_file": "Arquivo executável", - "git.filemode.symbolic_link": "Link simbólico", - "git.filemode.submodule": "Submódulo" + "git.filemode.submodule": "Submódulo", + "actions.general.token_permissions.mode.restricted": "Restrito" } diff --git a/options/locale/locale_pt-PT.json b/options/locale/locale_pt-PT.json index a45b495fa8..788004c308 100644 --- a/options/locale/locale_pt-PT.json +++ b/options/locale/locale_pt-PT.json @@ -81,9 +81,11 @@ "retry": "Tentar novamente", "rerun": "Reexecutar", "rerun_all": "Reexecutar todos os trabalhos", + "rerun_failed": "Reexecutar os trabalhos que falharam", "save": "Guardar", "add": "Adicionar", "add_all": "Adicionar tudo", + "dismiss": "Descartar", "remove": "Remover", "remove_all": "Remover tudo", "remove_label_str": "Remover item \"%s\"", @@ -103,6 +105,7 @@ "copy_error": "Falha ao copiar", "copy_type_unsupported": "Este tipo de ficheiro não pode ser copiado", "copy_filename": "Copiar o nome do ficheiro", + "copy_output": "Copiar a saída", "write": "Escrever", "preview": "Pré-visualizar", "loading": "Carregando…", @@ -120,6 +123,8 @@ "unpin": "Desafixar", "artifacts": "Artefactos", "expired": "Expirado", + "artifact_expires_at": "Expira em %s", + "artifact_expired_at": "Expira em %s", "confirm_delete_artifact": "Tem a certeza que quer eliminar este artefacto \"%s\"?", "archived": "Arquivado", "concept_system_global": "Global", @@ -148,21 +153,31 @@ "filter.private": "Privado", "no_results_found": "Não foram encontrados quaisquer resultados.", "internal_error_skipped": "Ocorreu um erro interno mas foi ignorado: %s", + "characters_spaces": "Espaços", + "characters_tabs": "Tabulações", + "text_indent_style": "Estilo da indentação", + "text_indent_size": "Tamanho da indentação", + "text_line_wrap": "Translinear", + "text_line_nowrap": "Não translinear", + "text_line_wrap_mode": "Modo de translineação", "search.search": "Pesquisar…", "search.type_tooltip": "Tipo de pesquisa", "search.fuzzy": "Aproximada", "search.fuzzy_tooltip": "Incluir também os resultados que estejam próximos do termo de pesquisa", "search.words": "Palavras", "search.words_tooltip": "Incluir apenas os resultados que correspondam às palavras do termo de pesquisa", - "search.regexp": "Regexp", + "search.regexp": "Expressão regular", "search.regexp_tooltip": "Incluir apenas os resultados que correspondam ao termo de pesquisa com expressões regulares", "search.exact": "Fiel", "search.exact_tooltip": "Incluir somente os resultados que correspondam rigorosamente ao termo de pesquisa", "search.repo_kind": "Pesquisar repositórios…", "search.user_kind": "Pesquisar utilizadores…", + "search.badge_kind": "Procurar crachás…", "search.org_kind": "Pesquisar organizações…", "search.team_kind": "Pesquisar equipas…", "search.code_kind": "Pesquisar código-fonte…", + "search.code_empty": "Iniciar uma nova pesquisa no código-fonte.", + "search.code_empty_description": "Insira uma palavra-chave para pesquisar no código-fonte.", "search.code_search_unavailable": "A pesquisa de código não está disponível, neste momento. Entre em contacto com o administrador.", "search.code_search_by_git_grep": "Os resultados da pesquisa no código-fonte neste momento são fornecidos pelo \"git grep\". Esses resultados podem ser melhores se o administrador habilitar o indexador do repositório.", "search.package_kind": "Pesquisar pacotes…", @@ -203,11 +218,15 @@ "editor.buttons.switch_to_legacy.tooltip": "Usar o editor clássico", "editor.buttons.enable_monospace_font": "Habilitar tipo de letra mono-espaçado", "editor.buttons.disable_monospace_font": "Desabilitar tipo de letra mono-espaçado", + "editor.code_editor.command_palette": "Paleta de comandos", + "editor.code_editor.find": "Procurar", + "editor.code_editor.placeholder": "Insira o conteúdo do ficheiro aqui", "filter.string.asc": "A–Z", "filter.string.desc": "Z–A", "error.occurred": "Ocorreu um erro", "error.report_message": "Se acredita tratar-se de um erro do Gitea, procure questões relacionadas no GitHub ou abra uma nova questão, se necessário.", "error.not_found": "Não foi possível encontrar o destino.", + "error.permission_denied": "Permissão negada.", "error.network_error": "Erro de rede", "startpage.app_desc": "Um serviço Git auto-hospedado e fácil de usar", "startpage.install": "Fácil de instalar", @@ -217,7 +236,7 @@ "startpage.lightweight": "Leve", "startpage.lightweight_desc": "Gitea requer poucos recursos e pode correr num simples Raspberry Pi. Economize a energia da sua máquina!", "startpage.license": "Código aberto", - "startpage.license_desc": "Vá buscar %[2]s! Junte-se a nós dando a sua contribuição para tornar este programa ainda melhor. Não se acanhe e contribua!", + "startpage.license_desc": "Vá buscar %[2]s! Junte-se a nós dando a sua contribuição para tornar este programa ainda melhor. Não hesite e contribua!", "install.install": "Instalação", "install.installing_desc": "Instalando agora, por favor aguarde…", "install.title": "Configuração inicial", @@ -254,7 +273,7 @@ "install.lfs_path": "Localização do Git LFS", "install.lfs_path_helper": "Os ficheiros rastreados pelo Git LFS serão armazenados nesta pasta. Deixe em branco para desabilitar.", "install.run_user": "Executar com este utilizador", - "install.run_user_helper": "O nome de utilizador do sistema operativo que vai executar o Gitea. Note que esse utilizador tem que ter acesso à localização base dos repositórios.", + "install.run_user_helper": "O nome de utilizador do sistema operativo que vai executar o Gitea, que tem de ter acesso de escrita nos caminhos dos dados. Este nome é detectado automaticamente e não pode ser alterado aqui. Para usar um utilizador diferente, reinicie o Gitea com essa conta.", "install.domain": "Domínio do servidor", "install.domain_helper": "Domínio ou endereço do servidor.", "install.ssh_port": "Porto do servidor SSH", @@ -277,12 +296,6 @@ "install.register_confirm": "Exigir confirmação de email para se inscrever", "install.mail_notify": "Habilitar notificações por email", "install.server_service_title": "Configurações do servidor e de terceiros", - "install.offline_mode": "Habilitar o modo local", - "install.offline_mode_popup": "Desabilitar redes de entrega de conteúdos de terceiros e servir localmente todos os recursos.", - "install.disable_gravatar": "Desabilitar o Gravatar", - "install.disable_gravatar_popup": "Desabilitar o Gravatar e fontes de avatares de terceiros. Será usado um avatar padrão, a não ser que o utilizador carregue um avatar localmente.", - "install.federated_avatar_lookup": "Habilitar avatares federados", - "install.federated_avatar_lookup_popup": "Habilitar pesquisa de avatares federada usando o Libravatar.", "install.disable_registration": "Desabilitar a auto-inscrição", "install.disable_registration_popup": "Desabilitar a auto-inscrição do utilizador. Somente os administradores poderão criar novas contas de utilizador.", "install.allow_only_external_registration_popup": "Permitir a inscrição somente por meio de serviços externos", @@ -302,12 +315,10 @@ "install.admin_email": "Endereço de email", "install.install_btn_confirm": "Instalar Gitea", "install.test_git_failed": "Não foi possível testar o comando 'git': %v", - "install.sqlite3_not_available": "Esta versão do Gitea não suporta o SQLite3. Descarregue a versão binária oficial em %s (não utilize a versão 'gobuild').", "install.invalid_db_setting": "As configurações da base de dados são inválidas: %v", "install.invalid_db_table": "A tabela \"%s\" da base de dados é inválida: %v", "install.invalid_repo_path": "A localização base dos repositórios é inválida: %v", "install.invalid_app_data_path": "A localização dos dados da aplicação é inválido: %v", - "install.run_user_not_match": "O nome de utilizador para 'executar como' não é o nome de utilizador corrente: %s → %s", "install.internal_token_failed": "Falha ao gerar o código interno: %v", "install.secret_key_failed": "Falha ao gerar a chave secreta: %v", "install.save_config_failed": "Falhou ao guardar a configuração: %v", @@ -540,6 +551,7 @@ "form.glob_pattern_error": " o padrão glob é inválido: %s.", "form.regex_pattern_error": " o padrão regex é inválido: %s.", "form.username_error": " só pode conter caracteres alfanuméricos ('0-9','a-z','A-Z'), hífen ('-'), sublinhado ('_') e ponto ('.') Não pode começar nem terminar com caracteres não alfanuméricos e caracteres não alfanuméricos consecutivos também são proibidos.", + "form.invalid_slug_error": " é inválido.", "form.invalid_group_team_map_error": " o mapeamento é inválido: %s", "form.unknown_error": "Erro desconhecido:", "form.captcha_incorrect": "O código CAPTCHA está errado.", @@ -628,14 +640,8 @@ "user.block.unblock.failure": "Falhou o desbloqueio do utilizador: %s", "user.block.blocked": "Bloqueou este utilizador.", "user.block.title": "Bloquear um utilizador", - "user.block.info": "Bloquear um utilizador evita que este interaja com repositórios, tal como abrir ou comentar em pedidos de integração ou questões. Saiba mais sobre como bloquear um utilizador.", - "user.block.info_1": "Bloquear um utilizador impede as seguintes operações na sua conta e nos seus repositórios:", - "user.block.info_2": "seguir a sua conta", - "user.block.info_3": "enviar-lhe notificações ao @mencionar o seu nome de utilizador", - "user.block.info_4": "convidá-lo/a para ser colaborador/a nos repositórios dele/dela", - "user.block.info_5": "juntar aos favoritos, derivar ou vigiar repositórios", - "user.block.info_6": "abrir e comentar questões ou pedidos de integração", - "user.block.info_7": "reagir aos seus comentários em questões ou pedidos de integração", + "user.block.info": "Bloquear um utilizador impede que este interaja com os repositórios, tal como abrir ou comentar um pedido de integração ou uma questão.", + "user.block.info.docs": "Saiba mais sobre como bloquear um utilizador.", "user.block.user_to_block": "Utilizador a bloquear", "user.block.note": "Nota", "user.block.note.title": "Nota opcional:", @@ -643,12 +649,13 @@ "user.block.note.edit": "Editar nota", "user.block.list": "Utilizadores bloqueados", "user.block.list.none": "Você ainda não bloqueou quaisquer utilizadores.", + "settings.general": "Geral", "settings.profile": "Perfil", "settings.account": "Conta", "settings.appearance": "Aparência", "settings.password": "Senha", "settings.security": "Segurança", - "settings.avatar": "Avatar", + "settings.avatar": "Foto do perfil", "settings.ssh_gpg_keys": "Chaves SSH / GPG", "settings.social": "Contas sociais", "settings.applications": "Aplicações", @@ -751,6 +758,7 @@ "settings.add_email": "Adicionar endereço de email", "settings.add_openid": "Adicionar URI OpenID", "settings.add_email_confirmation_sent": "Um email de confirmação foi enviado para \"%s\". Verifique a sua caixa de entrada dentro de %s para confirmar o seu endereço de email.", + "settings.email_primary_not_found": "Não foi possível encontrar o endereço de email escolhido.", "settings.add_email_success": "O novo endereço de email foi adicionado.", "settings.email_preference_set_success": "As preferências relativas ao email foram definidas com sucesso.", "settings.add_openid_success": "O novo endereço OpenID foi adicionado.", @@ -962,7 +970,6 @@ "repo.visibility_description": "Somente o proprietário ou os membros da organização, se tiverem direitos, poderão vê-lo.", "repo.visibility_helper": "Tornar o repositório privado", "repo.visibility_helper_forced": "O administrador obriga a que os repositórios novos sejam privados.", - "repo.visibility_fork_helper": "(alterar este parâmetro irá alterar também todas as derivações)", "repo.clone_helper": "Precisa de ajuda para clonar? Visite a Ajuda.", "repo.fork_repo": "Derivar repositório", "repo.fork_from": "Derivar de", @@ -977,6 +984,7 @@ "repo.fork.blocked_user": "Não pode derivar o repositório porque foi bloqueado/a pelo/a proprietário/a do repositório.", "repo.use_template": "Usar este modelo", "repo.open_with_editor": "Abrir com %s", + "repo.download_directory_as": "Descarregar pasta como %s", "repo.download_zip": "Descarregar ZIP", "repo.download_tar": "Descarregar TAR.GZ", "repo.download_bundle": "Descarregar PACOTE", @@ -1033,6 +1041,7 @@ "repo.forks": "Derivações", "repo.stars": "Favoritos", "repo.reactions_more": "e mais %d", + "repo.reactions": "Respostas", "repo.unit_disabled": "O administrador desabilitou esta secção do repositório.", "repo.language_other": "Outros", "repo.adopt_search": "Insira o nome de utilizador para procurar repositórios adoptados… (deixe em branco para encontrar todos)", @@ -1053,8 +1062,8 @@ "repo.transfer.accept_desc": "Transferir para \"%s\"", "repo.transfer.reject": "Rejeitar transferência", "repo.transfer.reject_desc": "Cancelar transferência para \"%s\"", - "repo.transfer.no_permission_to_accept": "Você não tem permissão para aceitar esta transferência.", - "repo.transfer.no_permission_to_reject": "Você não tem permissão para rejeitar esta transferência.", + "repo.transfer.is_transferring": "Transferindo…", + "repo.transfer.is_transferring_prompt": "O repositório está a ser transferido para %s", "repo.desc.private": "Privado", "repo.desc.public": "Público", "repo.desc.public_access": "Acesso público", @@ -1068,7 +1077,7 @@ "repo.template.git_hooks_tooltip": "Neste momento não pode modificar ou remover Automatismos do Git depois de adicionados. Escolha esta opção somente se confiar no repositório modelo.", "repo.template.webhooks": "Automatismos web", "repo.template.topics": "Tópicos", - "repo.template.avatar": "Avatar", + "repo.template.avatar": "Foto do perfil", "repo.template.issue_labels": "Rótulos das questões", "repo.template.one_item": "Tem que escolher pelo menos um item do modelo", "repo.template.invalid": "Tem que escolher um repositório modelo", @@ -1205,7 +1214,7 @@ "repo.ambiguous_runes_description": "Este ficheiro contém caracteres Unicode que podem ser confundidos com outros caracteres. Se acha que é intencional, pode ignorar este aviso com segurança. Use o botão Revelar para os mostrar.", "repo.invisible_runes_line": "Esta linha tem caracteres unicode invisíveis", "repo.ambiguous_runes_line": "Esta linha tem caracteres unicode ambíguos", - "repo.ambiguous_character": "%[1]c [U+%04[1]X] pode confundir-se com %[2]c [U+%04[2]X]", + "repo.ambiguous_character": "%[1]s pode confundir-se com %[2]s", "repo.escape_control_characters": "Revelar", "repo.unescape_control_characters": "Esconder", "repo.file_copy_permalink": "Copiar ligação permanente", @@ -1298,7 +1307,7 @@ "repo.editor.upload_file_is_locked": "O ficheiro \"%s\" está bloqueado por %s.", "repo.editor.upload_files_to_dir": "Carregar ficheiros para \"%s\"", "repo.editor.cannot_commit_to_protected_branch": "Não é possível cometer para o ramo protegido \"%s\".", - "repo.editor.no_commit_to_branch": "Não é possível cometer imediatamente para o ramo porque:", + "repo.editor.no_commit_to_branch": "Não é permitido cometer imediatamente para o ramo porque:", "repo.editor.user_no_push_to_branch": "O utilizador não pode enviar para o ramo", "repo.editor.require_signed_commit": "O ramo requer um cometimento assinado", "repo.editor.cherry_pick": "Escolher a dedo %s para:", @@ -1313,6 +1322,7 @@ "repo.editor.fork_branch_exists": "O ramo \"%s\" já existe na sua derivação. Escolha outro nome para o ramo.", "repo.commits.desc": "Navegar pelo histórico de modificações no código fonte.", "repo.commits.commits": "Cometimentos", + "repo.commits.history_enable_follow_renames": "Incluir renomeações", "repo.commits.no_commits": "Não há cometimentos em comum. \"%s\" e \"%s\" têm históricos completamente diferentes.", "repo.commits.nothing_to_compare": "Estes ramos são iguais.", "repo.commits.search.tooltip": "Pode prefixar palavras-chave com \"author:\", \"committer:\", \"after:\", ou \"before:\". Por exemplo: \"revert author:Alice before:2019-01-13\".", @@ -1346,10 +1356,13 @@ "repo.projects.desc": "Gerir questões e integrações nos planeamentos.", "repo.projects.description": "Descrição (opcional)", "repo.projects.description_placeholder": "Descrição", + "repo.projects.empty": "Ainda não há planeamentos.", + "repo.projects.empty_description": "Crie um planeamento para coordenar questões e pedidos de integração.", "repo.projects.create": "Criar planeamento", "repo.projects.title": "Título", "repo.projects.new": "Novo planeamento", "repo.projects.new_subheader": "Coordene, acompanhe e modifique o seu trabalho num só lugar, para que os planeamentos se mantenham transparentes e cumpram o calendário.", + "repo.projects.no_results": "Não existem planeamentos que correspondam à sua pesquisa.", "repo.projects.create_success": "O planeamento \"%s\" foi criado.", "repo.projects.deletion": "Eliminar planeamento", "repo.projects.deletion_desc": "Eliminar um planeamento remove-o de todas as questões relacionadas. Continuar?", @@ -1374,6 +1387,7 @@ "repo.projects.column.delete": "Eliminar coluna", "repo.projects.column.deletion_desc": "Eliminar uma coluna de um planeamento faz com que todas as questões que nela constam sejam movidas para a coluna predefinida. Continuar?", "repo.projects.column.color": "Colorido", + "repo.projects.column": "Coluna", "repo.projects.open": "Abrir", "repo.projects.close": "Fechar", "repo.projects.column.assigned_to": "Atribuída a", @@ -1391,11 +1405,12 @@ "repo.issues.new": "Questão nova", "repo.issues.new.title_empty": "O título não pode estar vazio", "repo.issues.new.labels": "Rótulos", - "repo.issues.new.no_label": "Sem rótulo", + "repo.issues.new.no_labels": "Sem rótulos", "repo.issues.new.clear_labels": "Retirar rótulos", "repo.issues.new.projects": "Planeamentos", "repo.issues.new.clear_projects": "Limpar planeamentos", - "repo.issues.new.no_projects": "Nenhum planeamento", + "repo.issues.new.no_projects": "Sem planeamentos", + "repo.issues.new.no_column": "Sem coluna", "repo.issues.new.open_projects": "Planeamentos abertos", "repo.issues.new.closed_projects": "Planeamentos fechados", "repo.issues.new.no_items": "Sem itens", @@ -1489,6 +1504,7 @@ "repo.issues.filter_sort.feweststars": "Favorito (crescente)", "repo.issues.filter_sort.mostforks": "Mais derivações", "repo.issues.filter_sort.fewestforks": "Menos derivações", + "repo.issues.quick_goto": "Ir para a questão", "repo.issues.action_open": "Abrir", "repo.issues.action_close": "Fechar", "repo.issues.action_label": "Rótulo", @@ -1514,16 +1530,18 @@ "repo.issues.commented_at": "comentou %s", "repo.issues.delete_comment_confirm": "Tem a certeza que quer eliminar este comentário?", "repo.issues.context.copy_link": "Copiar ligação", + "repo.issues.context.copy_source": "Copiar a fonte", "repo.issues.context.quote_reply": "Citar resposta", "repo.issues.context.reference_issue": "Criar uma nova questão referindo esta", "repo.issues.context.edit": "Editar", "repo.issues.context.delete": "Eliminar", "repo.issues.no_content": "Nenhuma descrição fornecida.", + "repo.issues.comment_no_content": "Não foi fornecido qualquer comentário.", "repo.issues.close": "Encerrar questão", "repo.issues.comment_pull_merged_at": "cometimento %[1]s integrado em %[2]s %[3]s", "repo.issues.comment_manually_pull_merged_at": "cometimento %[1]s integrado manualmente em %[2]s %[3]s", "repo.issues.close_comment_issue": "Fechar com comentário", - "repo.issues.reopen_issue": "Reabrir", + "repo.issues.reopen_issue": "Reabrir questão", "repo.issues.reopen_comment_issue": "Reabrir com comentário", "repo.issues.create_comment": "Comentar", "repo.issues.comment.blocked_user": "Não pode criar ou editar o comentário porque foi bloqueado/a pelo remetente ou pelo/a proprietário/a do repositório.", @@ -1701,6 +1719,7 @@ "repo.issues.review.content.empty": "Tem que deixar um comentário indicando a(s) modificação(ões) solicitada(s).", "repo.issues.review.reject": "modificações solicitadas %s", "repo.issues.review.wait": "foi solicitada para revisão %s", + "repo.issues.review.codeowners_rules": "Regras do CODEOWNERS", "repo.issues.review.add_review_request": "solicitou revisão de %s %s", "repo.issues.review.remove_review_request": "removeu a solicitação de revisão para %s %s", "repo.issues.review.remove_review_request_self": "recusou rever %s", @@ -1736,8 +1755,11 @@ "repo.issues.reference_link": "Referência: %s", "repo.compare.compare_base": "base", "repo.compare.compare_head": "comparar", + "repo.compare.title": "Comparação de modificações", + "repo.compare.description": "Escolha dois ramos ou duas etiquetas para ver o que mudou ou para iniciar um novo pedido de integração.", "repo.pulls.desc": "Habilitar pedidos de integração e revisão de código-fonte.", "repo.pulls.new": "Novo pedido de integração", + "repo.pulls.new.description": "Discuta e reveja com outras pessoas as modificações visíveis nesta comparação.", "repo.pulls.new.blocked_user": "Não pode criar o pedido de integração porque foi bloqueado/a pelo/a proprietário/a do repositório.", "repo.pulls.new.must_collaborator": "Tem de ser um/a colaborador/a para criar um pedido de integração.", "repo.pulls.new.already_existed": "Já existe um pedido de integração entre estes ramos", @@ -1747,7 +1769,6 @@ "repo.pulls.allow_edits_from_maintainers": "Permitir edições por parte dos responsáveis", "repo.pulls.allow_edits_from_maintainers_desc": "Utilizadores com acesso de escrita no ramo base também podem fazer envios para este ramo", "repo.pulls.allow_edits_from_maintainers_err": "Não foi possível fazer a modificação", - "repo.pulls.compare_changes_desc": "Escolha o ramo de destino e o ramo de origem.", "repo.pulls.has_viewed_file": "Visto", "repo.pulls.has_changed_since_last_review": "Alterado desde a última revisão", "repo.pulls.viewed_files_label": "%[1]d / %[2]d ficheiros vistos", @@ -1766,6 +1787,7 @@ "repo.pulls.review_only_possible_for_full_diff": "A revisão só é possível ao visualizar o diff completo", "repo.pulls.filter_changes_by_commit": "Filtrar por cometimento", "repo.pulls.nothing_to_compare": "Estes ramos são iguais. Não há necessidade de criar um pedido de integração.", + "repo.pulls.no_common_history": "Estes ramos não partilham uma base comum de integração. Escolha uma base diferente ou compare o ramo.", "repo.pulls.nothing_to_compare_have_tag": "Os ramos/etiquetas escolhidos são iguais.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "Estes ramos são iguais. Este pedido de integração ficará vazio.", "repo.pulls.has_pull_request": "Já existe um pedido de integração entre estes ramos: %[2]s#%[3]d", @@ -1773,6 +1795,8 @@ "repo.pulls.title_desc": "quer integrar %[1]d cometimento(s) do ramo %[2]s no ramo %[3]s", "repo.pulls.merged_title_desc": "integrou %[1]d cometimento(s) do ramo %[2]s no ramo %[3]s %[4]s", "repo.pulls.change_target_branch_at": "mudou o ramo de destino de %s para %s %s", + "repo.pulls.marked_as_work_in_progress_at": "marcou o pedido de integração como trabalho em andamento %s", + "repo.pulls.marked_as_ready_for_review_at": "marcou o pedido de integração como estando pronto para revisão %s", "repo.pulls.tab_conversation": "Diálogo", "repo.pulls.tab_commits": "Cometimentos", "repo.pulls.tab_files": "Ficheiros modificados", @@ -1791,12 +1815,14 @@ "repo.pulls.remove_prefix": "Remover o prefixo %s", "repo.pulls.data_broken": "Este pedido de integração está danificado devido à falta de informação da derivação.", "repo.pulls.files_conflicted": "Este pedido de integração contém modificações que entram em conflito com o ramo de destino.", + "repo.pulls.files_conflicted_no_listed_files": "(nenhum ficheiro em conflito listado)", "repo.pulls.is_checking": "Verificando se existem conflitos na integração…", "repo.pulls.is_ancestor": "Este ramo já está incluído no ramo de destino. Não há nada a integrar.", "repo.pulls.is_empty": "As modificações feitas neste ramo já existem no ramo de destino. Este cometimento ficará vazio.", "repo.pulls.required_status_check_failed": "Algumas das verificações obrigatórias não foram bem sucedidas.", "repo.pulls.required_status_check_missing": "Estão faltando algumas verificações necessárias.", "repo.pulls.required_status_check_administrator": "Uma vez que é administrador, ainda pode realizar a integração deste pedido.", + "repo.pulls.required_status_check_bypass_allowlist": "Você tem permissão para contornar as regras de salvaguarda para esta integração.", "repo.pulls.blocked_by_approvals": "Este pedido de integração ainda não tem aprovações necessárias suficientes. Já foram concedidas %d de um total de %d aprovações oficiais.", "repo.pulls.blocked_by_approvals_whitelisted": "Este pedido de integração ainda não tem um número de aprovações suficiente. %d em %d aprovações atribuídas pelos utilizadores ou equipas da lista de permissões.", "repo.pulls.blocked_by_rejection": "Este pedido de integração tem modificações solicitadas por um revisor oficial.", @@ -1829,6 +1855,7 @@ "repo.pulls.merge_manually": "Integrar manualmente", "repo.pulls.merge_commit_id": "O ID de cometimento da integração", "repo.pulls.require_signed_wont_sign": "O ramo requer que os cometimentos sejam assinados mas esta integração não vai ser assinada", + "repo.pulls.require_signed_head_commits_unverified": "O ramo exige que os cometimentos sejam assinados mas há pelo menos um cometimento deste pedido de integração que não foi verificado", "repo.pulls.invalid_merge_option": "Não pode usar esta opção de integração neste pedido de integração.", "repo.pulls.merge_conflict": "A integração falhou: Houve um conflito durante a integração. Dica: Tente uma estratégia diferente.", "repo.pulls.merge_conflict_summary": "Mensagem de erro", @@ -1845,7 +1872,8 @@ "repo.pulls.status_checking": "Algumas verificações estão pendentes", "repo.pulls.status_checks_success": "Todas as verificações foram bem sucedidas", "repo.pulls.status_checks_warning": "Algumas verificações reportaram avisos", - "repo.pulls.status_checks_failure": "Algumas verificações falharam", + "repo.pulls.status_checks_failure_required": "Algumas das verificações obrigatórias falharam", + "repo.pulls.status_checks_failure_optional": "Algumas das verificações opcionais falharam", "repo.pulls.status_checks_error": "Algumas verificações reportaram erros", "repo.pulls.status_checks_requested": "Obrigatório", "repo.pulls.status_checks_details": "Detalhes", @@ -1860,6 +1888,7 @@ "repo.pulls.update_not_allowed": "Não tem autorização para sincronizar o ramo", "repo.pulls.outdated_with_base_branch": "Este ramo é obsoleto em relação ao ramo base", "repo.pulls.close": "Encerrar pedido de integração", + "repo.pulls.reopen": "Reabrir pedido de integração", "repo.pulls.closed_at": "fechou este pedido de integração %[2]s", "repo.pulls.reopened_at": "reabriu este pedido de integração %[2]s", "repo.pulls.cmd_instruction_hint": "Ver instruções para a linha de comandos", @@ -1932,7 +1961,6 @@ "repo.signing.wont_sign.headsigned": "A integração não irá ser assinada, uma vez que o cometimento de topo não foi assinado.", "repo.signing.wont_sign.commitssigned": "A integração não irá ser assinada, uma vez que nenhum dos cometimentos associados foram assinados.", "repo.signing.wont_sign.approved": "A integração não irá ser assinada, uma vez que o pedido de integração não foi assinado.", - "repo.signing.wont_sign.not_signed_in": "Não tem a sessão iniciada.", "repo.ext_wiki": "Acesso a wiki externo", "repo.ext_wiki.desc": "Ligação para um wiki externo.", "repo.wiki": "Wiki", @@ -2114,7 +2142,11 @@ "repo.settings.pulls_desc": "Habilitar pedidos de integração no repositório", "repo.settings.pulls.ignore_whitespace": "Ignorar espaços em branco nos conflitos", "repo.settings.pulls.enable_autodetect_manual_merge": "Habilitar a identificação automática de integrações manuais (obs.: nalguns casos especiais a avaliação pode ser errada)", + "repo.settings.pulls.allow_merge_update": "Permitir, em cada integração, a modificação do ramo usado nos pedidos de integração", "repo.settings.pulls.allow_rebase_update": "Habilitar a modificação do ramo do pedido de integração através da mudança de base", + "repo.settings.pulls.default_update_style": "Estilo de modificações do ramo principal", + "repo.settings.pulls.default_target_branch": "Ramo predefinido para novos pedidos de integração", + "repo.settings.pulls.default_target_branch_default": "Ramo predefinido (%s)", "repo.settings.pulls.default_delete_branch_after_merge": "Eliminar, por norma, o ramo do pedido de integração depois de finalizada a integração", "repo.settings.pulls.default_allow_edits_from_maintainers": "Permitir, por norma, que os responsáveis editem", "repo.settings.releases_desc": "Habilitar lançamentos no repositório", @@ -2154,7 +2186,8 @@ "repo.settings.transfer_abort_invalid": "Não pode cancelar a transferência de um repositório inexistente.", "repo.settings.transfer_abort_success": "A transferência de repositório para %s foi cancelada com sucesso.", "repo.settings.transfer_desc": "Transferir este repositório para um utilizador ou para uma organização na qual você tenha direitos de administrador.", - "repo.settings.transfer_form_title": "Insira o nome do repositório para confirmar:", + "repo.settings.enter_repo_name_to_confirm": "Insira o nome do repositório para confirmar:", + "repo.settings.enter_repo_full_name_to_confirm": "Insira o nome completo do repositório (proprietário/nome) para confirmar:", "repo.settings.transfer_in_progress": "Está a ser feita uma transferência. Cancele-a, por favor, se quiser transferir este repositório para outro utilizador.", "repo.settings.transfer_notices_1": "- Você perderá o acesso ao repositório se o transferir para um utilizador individual.", "repo.settings.transfer_notices_2": "- Você manterá o acesso ao repositório se o transferir para uma organização da qual você é (co-)proprietário(a).", @@ -2173,10 +2206,10 @@ "repo.settings.trust_model.collaborator.desc": "Assinaturas válidas dos colaboradores deste repositório serão marcadas como \"fiável\", independentemente de corresponderem, ou não, ao autor do cometimento. Caso contrário, assinaturas válidas serão marcadas como \"não fiável\" se a assinatura corresponder ao autor do cometimento e \"não corresponde\", se não corresponder.", "repo.settings.trust_model.committer": "Autor do cometimento", "repo.settings.trust_model.committer.long": "Autor do cometimento: Confiar nas assinaturas que correspondam aos autores dos cometimentos. Isto corresponde ao comportamento do GitHub e força a que os cometimentos assinados do Gitea tenham o Gitea como autor do cometimento.", - "repo.settings.trust_model.committer.desc": "Assinaturas válidas apenas serão marcadas como \"fiável\" se corresponderem ao autor do cometimento, caso contrário serão marcadas como \"não corresponde\". Isto irá forçar o Gitea a ser o autor do cometimento nos cometimentos assinados, ficando o autor real marcado como \"Co-autorado-por:\" e \"Co-cometido-por:\" no rodapé do cometimento. A chave padrão do Gitea tem que corresponder a um utilizador na base de dados.", + "repo.settings.trust_model.committer.desc": "Assinaturas válidas apenas serão marcadas como \"fiável\" se corresponderem ao autor do cometimento, caso contrário serão marcadas como \"não corresponde\". Isso irá forçar o Gitea a ser o autor do cometimento nos cometimentos assinados, ficando o autor real marcado como \"Co-autorado-por:\" no rodapé do cometimento. A chave padrão do Gitea tem que corresponder a um utilizador na base de dados.", "repo.settings.trust_model.collaboratorcommitter": "Colaborador + Autor do cometimento", "repo.settings.trust_model.collaboratorcommitter.long": "Colaborador + Autor do cometimento: Confiar nas assinaturas dos colaboradores que correspondam ao autor do cometimento", - "repo.settings.trust_model.collaboratorcommitter.desc": "Assinaturas válidas feitas por colaboradores deste repositório serão marcadas como \"fiável\" se corresponderem ao autor do cometimento. Caso contrário, assinaturas válidas serão marcadas como \"não fiável\" se a assinatura corresponder ao autor do cometimento e \"não corresponde\" se não corresponder. Isto irá forçar a que o Gitea seja marcado como sendo o autor do cometimento nos cometimentos assinados, ficando o autor real marcado como \"Co-autorado-por:\" e \"Co-cometido-por:\" no rodapé do cometimento. A chave padrão do Gitea tem que corresponder a um utilizador na base de dados.", + "repo.settings.trust_model.collaboratorcommitter.desc": "Assinaturas válidas feitas por colaboradores deste repositório serão marcadas como \"fiável\" se corresponderem ao autor do cometimento. Caso contrário, assinaturas válidas serão marcadas como \"não fiável\" se a assinatura corresponder ao autor do cometimento e \"não corresponde\" se não corresponder. Isto irá forçar a que o Gitea seja marcado como sendo o autor do cometimento nos cometimentos assinados, ficando o autor real marcado como \"Co-autorado-por:\" no rodapé do cometimento. A chave padrão do Gitea tem que corresponder a um utilizador na base de dados.", "repo.settings.wiki_delete": "Eliminar dados do wiki", "repo.settings.wiki_delete_desc": "Eliminar os dados do repositório do wiki é permanente e não pode ser revertido.", "repo.settings.wiki_delete_notices_1": "- Isso excluirá e desabilitará permanentemente o wiki do repositório para %s.", @@ -2219,7 +2252,6 @@ "repo.settings.webhook_deletion_success": "O automatismo web foi removido.", "repo.settings.webhook.test_delivery": "Testar o envio", "repo.settings.webhook.test_delivery_desc": "Testar este automatismo web com um evento de envio falso.", - "repo.settings.webhook.test_delivery_desc_disabled": "Para testar este automatismo web com um evento falso, habilite-o.", "repo.settings.webhook.request": "Pedido", "repo.settings.webhook.response": "Resposta", "repo.settings.webhook.headers": "Cabeçalhos", @@ -2230,13 +2262,14 @@ "repo.settings.webhook.delivery.success": "Foi adicionado um evento à fila de entrega. Pode demorar alguns segundos a aparecer no histórico de entregas.", "repo.settings.githooks_desc": "Os Automatismos do Git são executados pelo próprio Git. Pode editar os ficheiros de automatismo abaixo para configurar operações personalizadas.", "repo.settings.githook_edit_desc": "Se o automatismo estiver desligado, será apresentado um conteúdo de teste. Deixar o conteúdo em branco irá desabilitar este automatismo.", - "repo.settings.githook_name": "Nome do automatismo", - "repo.settings.githook_content": "Conteúdo do automatismo", "repo.settings.update_githook": "Modificar automatismo", "repo.settings.add_webhook_desc": "O Gitea irá enviar pedidos POST com um tipo de conteúdo especificado para o URL de destino. Leia mais no guia sobre automatismos web.", "repo.settings.payload_url": "URL de destino", "repo.settings.http_method": "Método HTTP", "repo.settings.content_type": "Tipo de conteúdo POST", + "repo.settings.webhook.name": "Nome do automatismo web", + "repo.settings.webhook.name_helper": "Pode, opcionalmente, dar um nome amigável a este automatismo web", + "repo.settings.webhook.name_empty": "Automatismo web sem nome", "repo.settings.secret": "Segredo", "repo.settings.webhook_secret_desc": "Se o servidor de automatismos web suportar a utilização de segredos, você pode seguir o manual do automatismo web e preencher um segredo aqui.", "repo.settings.slack_username": "Nome de utilizador", @@ -2385,6 +2418,11 @@ "repo.settings.protect_merge_whitelist_committers_desc": "Permitir que somente utilizadores ou equipas constantes na lista de permissão possam executar, neste ramo, integrações constantes em pedidos de integração.", "repo.settings.protect_merge_whitelist_users": "Utilizadores com permissão para executar integrações:", "repo.settings.protect_merge_whitelist_teams": "Equipas com permissão para executar integrações:", + "repo.settings.protect_bypass_allowlist": "Contornar a salvaguarda do ramo", + "repo.settings.protect_enable_bypass_allowlist": "Permitir que determinados utilizadores ou grupos contornem a salvaguarda do ramo", + "repo.settings.protect_enable_bypass_allowlist_desc": "Os utilizadores e os grupos constantes na lista podem fazer integrações e fazer envios mesmo quando, por norma, a exigência de aprovações ou as regras de salvaguarda de ficheiros o bloquearia.", + "repo.settings.protect_bypass_allowlist_users": "Lista de utilizadores que têm permissão para contornar as salvaguardas:", + "repo.settings.protect_bypass_allowlist_teams": "Lista de grupos que têm permissão para contornar as salvaguardas:", "repo.settings.protect_check_status_contexts": "Habilitar verificação de estado", "repo.settings.protect_status_check_patterns": "Padrões de verificação de estado:", "repo.settings.protect_status_check_patterns_desc": "Insira padrões para especificar que verificações de estado têm de passar antes que os ramos possam ser integrados num ramo correspondente a esta regra. Cada linha especifíca um padrão. Os padrões não podem estar em branco.", @@ -2426,8 +2464,9 @@ "repo.settings.block_outdated_branch": "Bloquear integração se o pedido de integração for obsoleto", "repo.settings.block_outdated_branch_desc": "A integração não será possível quando o ramo de topo estiver abaixo do ramo base.", "repo.settings.block_admin_merge_override": "Os administradores têm de cumprir as regras de salvaguarda dos ramos", - "repo.settings.block_admin_merge_override_desc": "Os administradores têm de cumprir as regras de salvaguarda do ramo e não as podem contornar.", - "repo.settings.default_branch_desc": "Escolha um ramo do repositório como sendo o predefinido para pedidos de integração e cometimentos:", + "repo.settings.block_admin_merge_override_desc": "Os administradores têm de cumprir as regras de salvaguarda do ramo e não as podem contornar. Caso os utilizadores ou os grupos estejam na lista de permissão, podem contornar essas regras, desde que a lista de permissão esteja habilitada.", + "repo.settings.default_branch_desc": "Escolha um ramo do repositório como sendo o predefinido para cometimentos.", + "repo.settings.default_target_branch_desc": "Os pedidos de integração podem usar um ramo de destino predefinido diferente se isso estiver definido na secção Pedidos de Integração das Configurações Avançadas do Repositório.", "repo.settings.merge_style_desc": "Estilos de integração", "repo.settings.default_merge_style_desc": "Tipo de integração predefinido", "repo.settings.choose_branch": "Escolha um ramo…", @@ -2456,7 +2495,10 @@ "repo.settings.visibility.private.text": "Mudar a visibilidade para privado irá tornar o repositório visível somente para membros autorizados e poderá remover a relação entre o repositório e derivações existentes, vigilâncias e favoritos.", "repo.settings.visibility.private.bullet_title": "Mudar a visibilidade para privado irá:", "repo.settings.visibility.private.bullet_one": "Tornar o repositório visível somente para membros autorizados.", - "repo.settings.visibility.private.bullet_two": "Poderá remover a relação entre o repositório e derivações, vigilâncias e favoritos.", + "repo.settings.visibility.private.bullet_two": "Aplicar a visibilidade às derivações e remover as vigilâncias e os favoritos.", + "repo.settings.visibility.private.stats_stars": "Este repositório tem %d marca(s) de favorito que poderá(ão) ser perdida(s).", + "repo.settings.visibility.private.stats_watchers": "Este repositório tem %d vigilante(s) que poderá(ão) ser perdido(s).", + "repo.settings.visibility.private.stats_forks": "Este repositório tem %d derivação(ões) associada(s).", "repo.settings.visibility.public.button": "Tornar público", "repo.settings.visibility.public.text": "Mudar a visibilidade para público irá tornar o repositório visível para qualquer pessoa.", "repo.settings.visibility.public.bullet_title": "Mudar a visibilidade para público irá:", @@ -2541,7 +2583,7 @@ "repo.diff.show_more": "Mostrar mais", "repo.diff.load": "Carregar diff", "repo.diff.generated": "gerado", - "repo.diff.vendored": "externo", + "repo.diff.vendored": "Externo", "repo.diff.comment.add_line_comment": "Adicionar comentário de linha", "repo.diff.comment.placeholder": "Deixar um comentário", "repo.diff.comment.add_single_comment": "Adicionar um único comentário", @@ -2557,6 +2599,9 @@ "repo.diff.review.reject": "Solicitar modificações", "repo.diff.review.self_approve": "Autores de pedidos de integração não podem aprovar o seu próprio pedido de integração", "repo.diff.committed_by": "cometido por", + "repo.diff.coauthored_by": "co-autorado por", + "repo.commits.avatar_stack_and": "e", + "repo.commits.avatar_stack_people": "%d pessoas", "repo.diff.protected": "Protegido", "repo.diff.image.side_by_side": "Lado a Lado", "repo.diff.image.swipe": "Deslizar", @@ -2641,7 +2686,7 @@ "repo.branch.restore_success": "O ramo \"%s\" foi restaurado.", "repo.branch.restore_failed": "Falhou a restauração do ramo \"%s\".", "repo.branch.protected_deletion_failed": "O ramo \"%s\" está protegido, não pode ser eliminado.", - "repo.branch.default_deletion_failed": "O ramo \"%s\" é o ramo principal, não pode ser eliminado.", + "repo.branch.default_deletion_failed": "O ramo \"%s\" é o ramo predefinido ou o ramo de destino dos pedidos de integração. Não pode ser eliminado.", "repo.branch.default_branch_not_exist": "O ramo predefinido \"%s\" não existe.", "repo.branch.restore": "Restaurar o ramo \"%s\"", "repo.branch.download": "Descarregar o ramo \"%s\"", @@ -2658,7 +2703,7 @@ "repo.branch.new_branch_from": "Criar um novo ramo a partir do ramo \"%s\"", "repo.branch.renamed": "O ramo %s foi renomeado para %s.", "repo.branch.rename_default_or_protected_branch_error": "Só os administradores é que podem renomear o ramo principal ou ramos protegidos.", - "repo.branch.rename_protected_branch_failed": "Este ramo está protegido por regras de salvaguarda baseadas em padrões glob.", + "repo.branch.rename_protected_branch_failed": "Falhou a renomeação do ramo por causa das regras de salvaguarda dos ramos.", "repo.branch.commits_divergence_from": "Divergência nos cometimentos: %[1]d atrás e %[2]d à frente de %[3]s", "repo.branch.commits_no_divergence": "Idêntico ao ramo %[1]s", "repo.tag.create_tag": "Criar etiqueta %s", @@ -2684,6 +2729,7 @@ "graphs.code_frequency.what": "frequência de programação", "graphs.contributors.what": "contribuições", "graphs.recent_commits.what": "cometimentos recentes", + "graphs.chart_zoom_hint": "arrastar: amplia, shift+arrastar: desloca, duplo-clique: reinicia visualização", "org.org_name_holder": "Nome da organização", "org.org_full_name_holder": "Nome completo da organização", "org.org_name_helper": "Nomes das organizações devem ser curtos e memoráveis.", @@ -2692,6 +2738,8 @@ "org.members": "Membros", "org.teams": "Equipas", "org.code": "Código-fonte", + "org.repos.empty": "Ainda não há repositórios.", + "org.repos.empty_description": "Crie um repositório para partilhar código-fonte com a organização.", "org.lower_members": "membros", "org.lower_repositories": "repositórios", "org.create_new_team": "Nova equipa", @@ -2754,9 +2802,9 @@ "org.settings.labels_desc": "Adicionar rótulos que possam ser usados em questões para todos os repositórios desta organização.", "org.members.membership_visibility": "Visibilidade da filiação:", "org.members.public": "Visível", - "org.members.public_helper": "tornar oculto", + "org.members.public_helper": "Tornar oculto", "org.members.private": "Oculto", - "org.members.private_helper": "tornar visível", + "org.members.private_helper": "Tornar visível", "org.members.member_role": "Função do membro:", "org.members.owner": "Proprietário(a)", "org.members.member": "Membro", @@ -2784,10 +2832,13 @@ "org.teams.no_desc": "Esta equipa não tem descrição", "org.teams.settings": "Configurações", "org.teams.owners_permission_desc": "Os proprietários têm acesso total a todos os repositórios e têm acesso de administrador à organização.", + "org.teams.owners_permission_suggestion": "Pode criar novas equipas para membros para obter um controlo de acesso mais detalhado.", "org.teams.members": "Membros da equipa", + "org.teams.manage_team_member": "Gerir equipas e membros", + "org.teams.manage_team_member_prompt": "Os membros são geridos através de equipas. Adicione utilizadores a uma equipa para os convidar para esta organização.", "org.teams.update_settings": "Modificar configurações", "org.teams.delete_team": "Eliminar equipa", - "org.teams.add_team_member": "Adicionar membro da equipa", + "org.teams.add_team_member": "Adicionar membro à equipa", "org.teams.invite_team_member": "Convidar para %s", "org.teams.invite_team_member.list": "Convites pendentes", "org.teams.delete_team_title": "Eliminar equipa", @@ -2814,6 +2865,14 @@ "org.teams.all_repositories_read_permission_desc": "Esta equipa atribui o acesso de leitura a todos os repositórios: os seus membros podem ver e clonar os repositórios.", "org.teams.all_repositories_write_permission_desc": "Esta equipa atribui o acesso de escrita a todos os repositórios: os seus membros podem ler de, e enviar para os repositórios.", "org.teams.all_repositories_admin_permission_desc": "Esta equipa atribui o acesso de administração a todos os repositórios: os seus membros podem ler de, enviar para, e adicionar colaboradores aos repositórios.", + "org.teams.visibility": "Visibilidade", + "org.teams.visibility_private": "Privada", + "org.teams.visibility_private_helper": "Visível apenas para membros da equipa e proprietários da organização.", + "org.teams.visibility_limited": "Limitada", + "org.teams.visibility_limited_helper": "Visível para todos os membros desta organização.", + "org.teams.visibility_public": "Pública", + "org.teams.visibility_public_helper": "Visível para qualquer utilizador registado.", + "org.teams.owners_visibility_fixed": "A visibilidade da equipa de proprietários não pode ser alterada.", "org.teams.invite.title": "Foi-lhe feito um convite para se juntar à equipa %s na organização%s.", "org.teams.invite.by": "Convidado(a) por %s", "org.teams.invite.description": "Clique no botão abaixo para se juntar à equipa.", @@ -2825,6 +2884,8 @@ "org.worktime.date_range_end": "Data do fim", "org.worktime.query": "Consulta", "org.worktime.time": "Tempo", + "org.worktime.empty": "Ainda não há dados sobre o tempo de trabalho.", + "org.worktime.empty_description": "Ajuste o intervalo de datas para visualizar o tempo rastreado.", "org.worktime.by_repositories": "Por repositórios", "org.worktime.by_milestones": "Por etapas", "org.worktime.by_members": "Por membros", @@ -2839,6 +2900,30 @@ "admin.hooks": "Automatismos web", "admin.integrations": "Integrações", "admin.authentication": "Fontes de autenticação", + "admin.badges": "Crachás", + "admin.badges.badges_manage_panel": "Gestão de crachás", + "admin.badges.details": "Detalhes do crachá", + "admin.badges.new_badge": "Criar um novo crachá", + "admin.badges.slug": "Identificador", + "admin.badges.slug_been_taken": "O identificador já foi tomado.", + "admin.badges.description": "Descrição", + "admin.badges.image_url": "URL da imagem", + "admin.badges.new_success": "O chachá \"%s\" foi criado.", + "admin.badges.update_success": "O crachá foi modificado.", + "admin.badges.deletion_success": "O crachá foi eliminado.", + "admin.badges.edit_badge": "Editar chachá", + "admin.badges.update_badge": "Modificar crachá", + "admin.badges.delete_badge": "Eliminar crachá", + "admin.badges.delete_badge_desc": "Tem a certeza que quer eliminar permanentemente este crachá?", + "admin.badges.users_with_badge": "Utilizadores com crachá: %s", + "admin.badges.not_found": "O crachá não foi encontrado.", + "admin.badges.user_already_has": "O utilizador já tem este crachá.", + "admin.badges.user_add_success": "O crachá foi atribuído ao utilizador com sucesso.", + "admin.badges.user_remove_success": "O crachá foi retirado ao utilizador com sucesso.", + "admin.badges.manage_users": "Gerir utilizadores", + "admin.badges.add_user": "Adicionar utilizador", + "admin.badges.remove_user": "Remover utilizador", + "admin.badges.delete_user_desc": "Tem a certeza que quer retirar este utilizador do crachá?", "admin.emails": "Endereços de email do utilizador", "admin.config": "Configuração", "admin.config_summary": "Resumo", @@ -3067,6 +3152,7 @@ "admin.auths.attribute_mail": "Atributo do email", "admin.auths.attribute_ssh_public_key": "Atributo da chave pública SSH", "admin.auths.attribute_avatar": "Atributo do avatar", + "admin.auths.ssh_keys_are_verified": "Chaves SSH no LDAP são consideradas como verificadas", "admin.auths.attributes_in_bind": "Buscar atributos no contexto do Bind DN", "admin.auths.allow_deactivate_all": "Permitir que um resultado de pesquisa vazio desabilite todos os utilizadores", "admin.auths.use_paged_search": "Usar pesquisa paginada", @@ -3114,6 +3200,8 @@ "admin.auths.oauth2_required_claim_name_helper": "Defina este nome para restringir o início de sessão desta fonte a utilizadores que tenham uma reivindicação com este nome", "admin.auths.oauth2_required_claim_value": "Valor de Reivindicação obrigatório", "admin.auths.oauth2_required_claim_value_helper": "Defina este valor para restringir o início de sessão desta fonte a utilizadores que tenham uma reivindicação com este nome e este valor", + "admin.auths.open_id_connect_external_id_claim": "Nome declarado de identificação externa (opcional)", + "admin.auths.open_id_connect_external_id_claim_helper": "Nome declarado como identidade externa do utilizador. O predefinido é \"sub\". Para Azure AD / Entra ID, defina o nome como \"oid\" para manter a continuidade ao migrar de um fornecedor de Azure AD V2. Nota: a declaração \"oid\" exige que o âmbito \"profile\" seja incluído no campo Âmbitos acima.", "admin.auths.oauth2_group_claim_name": "Reivindicar nome que fornece nomes de grupo para esta fonte. (Opcional)", "admin.auths.oauth2_full_name_claim_name": "Nome completo reivindicado. (Opcional — se for definido, o nome completo do utilizador será sempre sincronizado com este reivindicado)", "admin.auths.oauth2_ssh_public_key_claim_name": "Nome reivindicado da chave pública SSH", @@ -3166,11 +3254,8 @@ "admin.config.server_config": "Configuração do servidor", "admin.config.app_name": "Título do sítio", "admin.config.app_ver": "Versão do Gitea", - "admin.config.app_url": "URL base do Gitea", "admin.config.custom_conf": "Localização do ficheiro de configuração", "admin.config.custom_file_root_path": "Localização dos ficheiros personalizados", - "admin.config.domain": "Domínio do servidor", - "admin.config.offline_mode": "Modo local", "admin.config.disable_router_log": "Desabilitar registos do encaminhador", "admin.config.run_user": "Executa com este nome de utilizador", "admin.config.run_mode": "Modo de execução", @@ -3221,7 +3306,6 @@ "admin.config.default_enable_timetracking": "Habilitar, por norma, a contagem do tempo", "admin.config.default_allow_only_contributors_to_track_time": "Permitir a contagem de tempo somente aos contribuidores", "admin.config.no_reply_address": "Domínio dos emails ocultos", - "admin.config.default_visibility_organization": "Visibilidade predefinida para as novas organizações", "admin.config.default_enable_dependencies": "Habilitar, por norma, dependências nas questões", "admin.config.webhook_config": "Configuração do automatismo web", "admin.config.queue_length": "Tamanho da fila", @@ -3250,15 +3334,20 @@ "admin.config.cache_config": "Configuração de cache", "admin.config.cache_adapter": "Adaptador de cache", "admin.config.cache_interval": "Intervalo de cache", - "admin.config.cache_conn": "Conexão de cache", "admin.config.cache_item_ttl": "TTL do item de cache", "admin.config.cache_test": "Teste da cache", "admin.config.cache_test_failed": "Fahou ao sondar a cache: %v.", "admin.config.cache_test_slow": "O teste da cache foi bem sucedido, mas a resposta é lenta: %s.", "admin.config.cache_test_succeeded": "O teste da cache foi bem sucedido, obtive resposta em %s.", + "admin.config.common.start_time": "Hora do início", + "admin.config.common.end_time": "Hora do término", + "admin.config.common.skip_time_check": "Deixe a hora em branco (limpe o campo) para ignorar o controlo do tempo", + "admin.config.instance_maintenance": "Manutenção da instância", + "admin.config.instance_maintenance_mode.admin_web_access_only": "Permitir o acesso à interface web somente aos administradores", + "admin.config.instance_web_banner.enabled": "Mostrar faixa publicitária", + "admin.config.instance_web_banner.message_placeholder": "Mensagem da faixa publicitária (suporta markdown)", "admin.config.session_config": "Configuração de sessão", "admin.config.session_provider": "Fornecedor da sessão", - "admin.config.provider_config": "Configuração do fornecedor", "admin.config.cookie_name": "Nome do cookie", "admin.config.gc_interval_time": "Intervalo de tempo entre recolhas do lixo", "admin.config.session_life_time": "Tempo de vida da sessão", @@ -3266,7 +3355,7 @@ "admin.config.cookie_life_time": "Tempo de vida do cookie", "admin.config.picture_config": "Configuração da imagem e do avatar", "admin.config.picture_service": "Serviço de imagem", - "admin.config.disable_gravatar": "Desabilitar o Gravatar", + "admin.config.enable_gravatar": "Habilitar o Gravatar", "admin.config.enable_federated_avatar": "Habilitar avatares federados", "admin.config.open_with_editor_app_help": "Os editores de \"Abrir com\" do menu de clonagem. Se for deixado em branco, será usado o predefinido. Expanda para ver o predefinido.", "admin.config.git_guide_remote_name": "Nome remoto do repositório para os comandos git no guia", @@ -3278,8 +3367,6 @@ "admin.config.git_gc_args": "Argumentos da recolha de lixo", "admin.config.git_migrate_timeout": "Prazo da migração", "admin.config.git_mirror_timeout": "Prazo para sincronização da réplica", - "admin.config.git_clone_timeout": "Prazo da operação de clonagem", - "admin.config.git_pull_timeout": "Prazo da operação de puxar", "admin.config.git_gc_timeout": "Prazo da operação de recolha de lixo", "admin.config.log_config": "Configuração do registo", "admin.config.logger_name_fmt": "Registador: %s", @@ -3446,6 +3533,7 @@ "packages.dependencies": "Dependências", "packages.keywords": "Palavras-chave", "packages.details": "Detalhes", + "packages.name": "Nome do pacote", "packages.details.author": "Autor(a)", "packages.details.project_site": "Página web do projecto", "packages.details.repository_site": "Página web do repositório", @@ -3541,9 +3629,27 @@ "packages.swift.registry": "Configure este registo usando a linha de comandos:", "packages.swift.install": "Adicione o pacote no seu ficheiro Package.swift:", "packages.swift.install2": "e execute o seguinte comando:", + "packages.terraform.install": "Defina o seu estado para usar a infra-estrutura HTTP", + "packages.terraform.install2": "e execute o seguinte comando:", + "packages.terraform.lock_status": "Estado do bloqueio", + "packages.terraform.locked_by": "Bloqueado por %s", + "packages.terraform.unlocked": "Desbloqueado", + "packages.terraform.lock": "Bloquear", + "packages.terraform.unlock": "Desbloquear", + "packages.terraform.lock.success": "O estado do Terraform foi bloqueado com sucesso.", + "packages.terraform.unlock.success": "O estado do Terraform foi desbloqueado com sucesso.", + "packages.terraform.lock.error.already_locked": "O estado do Terraform já se encontra bloqueado.", + "packages.terraform.delete.locked": "O estado do Terraform está bloqueado e não pode ser eliminado.", + "packages.terraform.delete.latest": "A última versão de um estado do Terraform não pode ser eliminada.", "packages.vagrant.install": "Para adicionar uma máquina virtual Vagrant, execute o seguinte comando:", "packages.settings.link": "Vincular este pacote a um repositório", - "packages.settings.link.description": "Se você vincular um pacote a um repositório, o pacote será listado na lista de pacotes do repositório. Apenas os repositórios do mesmo dono podem ser vinculados. Deixar o campo em branco irá remover o vínculo.", + "packages.settings.link.description": "Se você vincular um pacote a um repositório, o pacote será apresentado na lista de pacotes do repositório.", + "packages.settings.link.notice1": "Só os repositórios que tenham o mesmo proprietário é que podem ser vinculados.", + "packages.settings.link.notice2": "Vincular um repositório não altera a visibilidade do pacote.", + "packages.settings.link.notice3": "Deixar o campo em branco remove o vínculo.", + "packages.settings.visibility": "Visibilidade do pacote", + "packages.settings.visibility.inherit": "A visibilidade do pacote é herdada do proprietário e não pode ser alterada aqui de forma independente. Para alterá-la, modifique as configurações de visibilidade do utilizador ou organização proprietários deste pacote.", + "packages.settings.visibility.button": "Alterar a visibilidade do proprietário", "packages.settings.link.select": "Escolha o repositório", "packages.settings.link.button": "Modificar vínculo ao repositório", "packages.settings.link.success": "O vínculo ao repositório foi modificado com sucesso.", @@ -3554,8 +3660,13 @@ "packages.settings.delete": "Eliminar pacote", "packages.settings.delete.description": "Eliminar o pacote é permanente e não pode ser desfeito.", "packages.settings.delete.notice": "Está prestes a eliminar %s (%s). Esta operação é irreversível. Tem a certeza?", + "packages.settings.delete.notice.package": "Está prestes a eliminar %s e todas as suas versões. Esta operação é irreversível. Tem a certeza?", "packages.settings.delete.success": "O pacote foi eliminado.", + "packages.settings.delete.version.success": "A versão do pacote foi eliminada.", "packages.settings.delete.error": "Falhou a eliminação do pacote.", + "packages.settings.delete.version": "Eliminar versão", + "packages.settings.delete.confirm": "Insira o nome do pacote para confirmar", + "packages.settings.delete.invalid_package_name": "O nome do pacote que inseriu está errado.", "packages.owner.settings.cargo.title": "Índice do registo do Cargo", "packages.owner.settings.cargo.initialize": "Inicializar índice", "packages.owner.settings.cargo.initialize.description": "É necessário um repositório Git de índice especial para usar o registo Cargo. Usar esta opção irá (re-)criar o repositório e configurá-lo automaticamente.", @@ -3612,6 +3723,7 @@ "actions.status.success": "Sucesso", "actions.status.failure": "Falha", "actions.status.cancelled": "Cancelado", + "actions.status.cancelling": "Cancelando", "actions.status.skipped": "Ignorado", "actions.status.blocked": "Bloqueado", "actions.runners": "Executores", @@ -3622,6 +3734,7 @@ "actions.runners.id": "ID", "actions.runners.name": "Nome", "actions.runners.owner_type": "Tipo", + "actions.runners.availability": "Disponibilidade", "actions.runners.description": "Descrição", "actions.runners.labels": "Rótulos", "actions.runners.last_online": "Última vez que esteve ligado", @@ -3637,6 +3750,12 @@ "actions.runners.update_runner": "Guardar alterações", "actions.runners.update_runner_success": "O executor foi modificado com sucesso", "actions.runners.update_runner_failed": "Falhou ao modificar o executor", + "actions.runners.enable_runner": "Habilitar este executor", + "actions.runners.enable_runner_success": "O executor foi habilitado com sucesso", + "actions.runners.enable_runner_failed": "Falhou ao habilitar o executor", + "actions.runners.disable_runner": "Desabilitar este executor", + "actions.runners.disable_runner_success": "O executor foi desabilitado com sucesso", + "actions.runners.disable_runner_failed": "Falhou ao desabilitar o executor", "actions.runners.delete_runner": "Eliminar este executor", "actions.runners.delete_runner_success": "O executor foi eliminado com sucesso", "actions.runners.delete_runner_failed": "Falhou ao eliminar o executor", @@ -3652,17 +3771,30 @@ "actions.runners.reset_registration_token_confirm": "Gostaria de invalidar o código vigente e gerar um novo?", "actions.runners.reset_registration_token_success": "O código de incrição do executor foi reposto com sucesso", "actions.runs.all_workflows": "Todas as sequências de trabalho", + "actions.runs.other_workflows": "Outras sequências de trabalho", + "actions.runs.other_workflows_tooltip": "Sequências de trabalho que foram executadas neste repositório mas que não existem no ramo principal.", + "actions.runs.workflow_run_count_1": "%d execução de sequência de trabalho", + "actions.runs.workflow_run_count_n": "%d execuções de sequências de trabalho", "actions.runs.commit": "Cometimento", + "actions.runs.run_details": "Detalhes da execução", + "actions.runs.workflow_file": "Ficheiro de sequência de trabalho", + "actions.runs.workflow_file_no_permission": "Sem permissão para ver o ficheiro da sequência de trabalho", "actions.runs.scheduled": "Agendadas", "actions.runs.pushed_by": "enviado por", "actions.runs.invalid_workflow_helper": "O ficheiro de configuração da sequência de trabalho é inválido. Verifique o seu ficheiro de configuração: %s", "actions.runs.no_matching_online_runner_helper": "Não existem executores ligados que tenham o rótulo %s", + "actions.runs.no_runner_online": "Não há executores ligados que possam pegar neste trabalho.", + "actions.runs.waiting_for_available_runner": "Aguardando que um executor compatível fique disponível.", + "actions.runs.waiting_for_dependent_jobs": "Aguardando a conclusão dos seguintes trabalhos: %s", "actions.runs.no_job_without_needs": "A sequência de trabalho tem que conter pelo menos um trabalho sem dependências.", "actions.runs.no_job": "A sequência de trabalho tem que conter pelo menos um trabalho", + "actions.runs.invalid_reusable_workflow_uses": "Sequência de trabalho \"uses\" inválida: %s", "actions.runs.actor": "Interveniente", "actions.runs.status": "Estado", "actions.runs.actors_no_select": "Todos os intervenientes", "actions.runs.status_no_select": "Todos os estados", + "actions.runs.branch": "Ramo", + "actions.runs.branches_no_select": "Todos os ramos", "actions.runs.no_results": "Nenhum resultado obtido.", "actions.runs.no_workflows": "Ainda não há sequências de trabalho.", "actions.runs.no_workflows.quick_start": "Não sabe como começar com o Gitea Actions? Veja o guia de inicio rápido.", @@ -3675,12 +3807,64 @@ "actions.runs.delete.description": "Tem a certeza que pretende eliminar permanentemente a execução desta sequência de trabalho? Esta operação não poderá ser desfeita.", "actions.runs.not_done": "A execução desta sequência de trabalho ainda não terminou.", "actions.runs.view_workflow_file": "Ver ficheiro da sequência de trabalho", + "actions.runs.summary": "Resumo", + "actions.runs.all_jobs": "Todos os trabalhos", + "actions.runs.job_summaries": "Resumos do trabalho", + "actions.runs.expand_caller_jobs": "Mostrar trabalhos deste chamador de sequência de trabalho reutilizável", + "actions.runs.collapse_caller_jobs": "Esconder trabalhos deste chamador de sequência de trabalho reutilizável", + "actions.runs.attempt": "Tentativa", + "actions.runs.latest": "Mais recente", + "actions.runs.latest_attempt": "Última tentativa", + "actions.runs.triggered_via": "Despoletado por %s", + "actions.runs.rerun_triggered": "Reexecução despoletada", + "actions.runs.back_to_pull_request": "Voltar ao pedido de integração", + "actions.runs.back_to_workflow": "Voltar à sequência de trabalho", + "actions.runs.total_duration": "Duração total", + "actions.runs.workflow_dependencies": "Dependências da sequência de trabalho", + "actions.runs.graph_jobs_count_1": "%d trabalho", + "actions.runs.graph_jobs_count_n": "%d trabalhos", + "actions.runs.graph_dependencies_count_1": "%d dependência", + "actions.runs.graph_dependencies_count_n": "%d dependências", + "actions.runs.graph_success_rate": "%s de sucesso", + "actions.runs.graph_zoom_in": "Aproximar (Ctrl/Cmd + roda do rato)", + "actions.runs.graph_zoom_max": "Já está ampliado a 100%", + "actions.runs.graph_zoom_out": "Afastar (Ctrl/Cmd + roda do rato)", + "actions.runs.graph_reset_view": "Reiniciar a visualização", "actions.workflow.disable": "Desabilitar sequência de trabalho", "actions.workflow.disable_success": "A sequência de trabalho '%s' foi desabilitada com sucesso.", "actions.workflow.enable": "Habilitar sequência de trabalho", "actions.workflow.enable_success": "A sequência de trabalho '%s' foi habilitada com sucesso.", "actions.workflow.disabled": "A sequência de trabalho está desabilitada.", + "actions.workflow.scope_owner": "Proprietário(a)", + "actions.workflow.scope_global": "Global", + "actions.workflow.required": "Obrigatório", + "actions.workflow.scoped_required_cannot_disable": "Esta sequência de trabalho de âmbito específico é obrigatória e não pode ser desabilitada.", + "actions.scoped_workflows": "Sequências de trabalho de âmbito específico", + "actions.scoped_workflows.desc_org": "Registe repositórios como fontes de sequências de trabalho de âmbito específico. Os ficheiros das sequências de trabalho dentro de pastas das sequências de trabalho de âmbito específico do ramo principal de um repositório fonte são executados em todos os repositórios desta organização, no próprio contexto desse repositório.", + "actions.scoped_workflows.desc_user": "Registe repositórios como fontes de sequências de trabalho de âmbito específico. Os ficheiros das sequências de trabalho dentro de pastas das sequências de trabalho de âmbito específico do ramo principal de um repositório fonte são executados em todos os seus repositórios, no próprio contexto desse repositório.", + "actions.scoped_workflows.desc_global": "Registe repositórios como fontes de sequências de trabalho de âmbito específico. Os ficheiros das sequências de trabalho dentro de pastas das sequências de trabalho de âmbito específico do ramo principal de um repositório fonte são executados em todos os repositórios desta instância, no próprio contexto desse repositório. Uma vez que as fontes ao nível da instância são avaliadas em todos os eventos do repositório, registá-las poderá acrescentar uma sobrecarga em instâncias grandes.", + "actions.scoped_workflows.add_help": "Para fornecer sequências de trabalho de âmbito específico a partir de um repositório, cometa os ficheiros da sequência de trabalho sob %s no seu ramo principal e depois registe o repositório como uma fonte abaixo.", + "actions.scoped_workflows.security_note": "O conteúdo da sequência de trabalho de um repositório de origem é executado em todos os repositórios aos quais se aplica e os seus scripts de etapas, bem como os seus resultados, são escritos nos registos das operações desse repositório e podem ser lidos por qualquer pessoa que tenha permissão para ver as operações do repositório consumidor. Portanto, registar um repositório privado como uma fonte divulga a lógica da sequência de trabalho através desses registos. Registe apenas repositórios cujo conteúdo da sequência de trabalho possa ser partilhado com todos os repositórios consumidores. Se uma sequência de trabalho de âmbito específico fizer referência a uma sequência de trabalho reutilizável de um repositório privado, certifique-se que todos os repositórios consumidores a podem ler, caso contrário a sequência de trabalho irá falhar aí.", + "actions.scoped_workflows.source.add": "Adicionar repositório de origem", + "actions.scoped_workflows.source.add_success": "Repositório de origem adicionado.", + "actions.scoped_workflows.source.remove_success": "Repositório de origem removido.", + "actions.scoped_workflows.source.not_found": "Repositório não encontrado.", + "actions.scoped_workflows.required.update_success": "As sequências de trabalho obrigatórias foram refrescadas.", + "actions.scoped_workflows.required.label": "Marcar sequências de trabalho como sendo obrigatórias (uma sequência de trabalho obrigatória não pode ser desabilitada pelos repositórios):", + "actions.scoped_workflows.required.patterns": "Padrões de verificação de estado obrigatórios", + "actions.scoped_workflows.required.patterns_aria": "Padrões de verificação de estado obrigatórios para %s", + "actions.scoped_workflows.required.patterns_note": "aplicada apenas enquanto a sequência de trabalho for obrigatória", + "actions.scoped_workflows.required.patterns_hint": "Marque a sequência de trabalho como sendo obrigatória para configurar os seus padrões de verificação de estado.", + "actions.scoped_workflows.required.patterns_help": "Um padrão de verificação de estado (glob) por linha. Um pedido de integração consumidor só pode ser executado depois de ter sido aprovado um estado que corresponda a todos os padrões. Esta regra é aplicada a qualquer ramo de destino que tenha uma regra de salvaguarda, mesmo que as suas próprias verificações de estado estejam desabilitadas; um ramo de destino sem regra de salvaguarda não está sujeito a restrições.", + "actions.scoped_workflows.required.patterns_empty": "Cada sequência de trabalho obrigatória precisa de pelo menos um padrão de verificação de estado.", + "actions.scoped_workflows.required.missing_file": "o ficheiro já não está na origem", + "actions.scoped_workflows.required.expected_contexts": "Verificações de estado esperadas (está marcada uma verificação que corresponde a um padrão)", + "actions.scoped_workflows.required.no_status_contexts": "Esta sequência de trabalho não faz verificações de estado; por isso, marcá-la como obrigatória impediria a execução de todos os pedidos de integração que a utilizam. Desmarque a opção «Obrigatória».", + "actions.scoped_workflows.no_files": "Não foram encontrados quaisquer ficheiros de sequência de trabalho de âmbito específico no ramo principal.", "actions.workflow.run": "Executar sequência de trabalho", + "actions.workflow.create_status_badge": "Criar distintivo de estado", + "actions.workflow.status_badge": "Distintivo de estado", + "actions.workflow.status_badge_url": "URL do distintivo", "actions.workflow.not_found": "A sequência de trabalho '%s' não foi encontrada.", "actions.workflow.run_success": "A sequência de trabalho '%s' foi executada com sucesso.", "actions.workflow.from_ref": "Usar sequência de trabalho de", @@ -3721,8 +3905,27 @@ "projects.exit_fullscreen": "Sair do ecrã inteiro", "git.filemode.changed_filemode": "%[1]s → %[2]s", "git.filemode.directory": "Pasta", - "git.filemode.normal_file": "Ficheiro normal", - "git.filemode.executable_file": "Ficheiro executável", + "git.filemode.normal_file": "Normal", + "git.filemode.executable_file": "Executável", "git.filemode.symbolic_link": "Ligação simbólica", - "git.filemode.submodule": "Submódulo" + "git.filemode.submodule": "Submódulo", + "org.repos.none": "Sem repositórios.", + "actions.general.permissions": "Permissões do código das operações", + "actions.general.token_permissions.mode": "Permissões predefinidas do código", + "actions.general.token_permissions.mode.desc": "Um trabalho das operações irá usar as permissões predefinidas se não as declarar no ficheiro da sequência de trabalho.", + "actions.general.token_permissions.mode.permissive": "Permissivo", + "actions.general.token_permissions.mode.permissive.desc": "Permissões de leitura e escrita para o repositório do trabalho.", + "actions.general.token_permissions.mode.restricted": "Restrito", + "actions.general.token_permissions.mode.restricted.desc": "Permissões apenas de leitura para unidades de conteúdos (código, lançamentos) do repositório do trabalho.", + "actions.general.token_permissions.override_owner": "Substituir a configuração ao nível do proprietário", + "actions.general.token_permissions.override_owner_desc": "Se estiver habilitado, este repositório irá usar a sua própria configuração das operações ao invés de seguir a configuração ao nível do proprietário (utilizador ou organização).", + "actions.general.token_permissions.maximum": "Permissões máximas do código", + "actions.general.token_permissions.maximum.description": "As permissões concretas para o trabalho das operações serão limitadas pelas permissões máximas.", + "actions.general.token_permissions.fork_pr_note": "Se o trabalho for iniciado por um pedido de integração de uma derivação, as suas permissões concretas não irão exceder as permissões apenas de leitura.", + "actions.general.token_permissions.customize_max_permissions": "Personalizar as permissões máximas", + "actions.general.cross_repo": "Acesso entre repositórios", + "actions.general.cross_repo_desc": "Permitir que os repositórios escolhidos sejam acedidos (somente leitura) por todos os repositórios deste proprietário com GITEA_TOKEN ao executar os trabalhos das operações.", + "actions.general.cross_repo_selected": "Repositórios escolhidos", + "actions.general.cross_repo_target_repos": "Repositórios de destino", + "actions.general.cross_repo_add": "Adicionar repositório de destino" } diff --git a/options/locale/locale_ru-RU.json b/options/locale/locale_ru-RU.json index 9dc16ab820..aebd8b9cbe 100644 --- a/options/locale/locale_ru-RU.json +++ b/options/locale/locale_ru-RU.json @@ -184,7 +184,6 @@ "install.lfs_path": "Путь к корневому каталогу Git LFS", "install.lfs_path_helper": "В этом каталоге будут храниться файлы Git LFS. Оставьте пустым, чтобы отключить LFS.", "install.run_user": "Запуск от имени пользователя", - "install.run_user_helper": "Имя пользователя операционной системы, под которым работает Gitea. Обратите внимание, что этот пользователь должен иметь доступ к корневому пути репозиториев.", "install.domain": "Домен сервера", "install.domain_helper": "Домен или адрес хоста для сервера.", "install.ssh_port": "Порт SSH сервера", @@ -205,12 +204,6 @@ "install.register_confirm": "Требовать подтверждение по электронной почте для регистрации", "install.mail_notify": "Разрешить почтовые уведомления", "install.server_service_title": "Сервер и настройки внешних служб", - "install.offline_mode": "Включить локальный режим", - "install.offline_mode_popup": "Отключить сторонние сети доставки контента и отдавать все ресурсы из их локальных копий.", - "install.disable_gravatar": "Отключить Gravatar", - "install.disable_gravatar_popup": "Отключить Gravatar и сторонние источники аватаров. Если пользователь не загрузит аватар локально, то по умолчанию будет использоваться стандартный аватар.", - "install.federated_avatar_lookup": "Включить федеративные аватары", - "install.federated_avatar_lookup_popup": "Включите поиск федеративного аватара для использования службы с открытым исходным кодом на основе libravatar.", "install.disable_registration": "Отключить самостоятельную регистрацию", "install.disable_registration_popup": "Отключить самостоятельную регистрацию. Только администраторы смогут создавать новые учётные записи пользователей.", "install.allow_only_external_registration_popup": "Разрешить регистрацию только через сторонние сервисы", @@ -230,12 +223,10 @@ "install.admin_email": "Адрес эл. почты", "install.install_btn_confirm": "Установить Gitea", "install.test_git_failed": "Не удалось проверить 'git' команду: %v", - "install.sqlite3_not_available": "Эта версия Gitea не поддерживает SQLite3. Пожалуйста, загрузите официальную бинарную версию из %s (не 'go build' версия).", "install.invalid_db_setting": "Некорректные настройки базы данных: %v", "install.invalid_db_table": "Таблица «%s» базы данных некорректна: %v", "install.invalid_repo_path": "Недопустимый путь к корню репозитория: %v", "install.invalid_app_data_path": "Неверный путь к приложению: %v", - "install.run_user_not_match": "Текущий пользователь не является пользователем для запуска: %s -> %s", "install.internal_token_failed": "Не удалось создать внутренний токен: %v", "install.secret_key_failed": "Не удалось создать секретный ключ: %v", "install.save_config_failed": "Не удалось сохранить конфигурацию: %v", @@ -689,7 +680,6 @@ "settings.permissions_list": "Разрешения:", "settings.manage_oauth2_applications": "Управление приложениями OAuth2", "settings.edit_oauth2_application": "Изменить приложение OAuth2", - "settings.oauth2_applications_desc": "Приложения OAuth2 позволяет стороннему приложению к безопасно аутентифицировать пользователей данной установки Gitea.", "settings.remove_oauth2_application": "Удалить приложение OAuth2", "settings.remove_oauth2_application_desc": "Удаление приложения OAuth2 отменит доступ ко всем подписанным токенам доступа. Продолжить?", "settings.remove_oauth2_application_success": "Приложение удалено.", @@ -706,7 +696,6 @@ "settings.oauth2_regenerate_secret_hint": "Потеряли свой ключ?", "settings.oauth2_client_secret_hint": "Пожалуйста, сохраните секрет, так как после закрытия или обновления страницы он больше не будет показан.", "settings.oauth2_application_edit": "Изменить", - "settings.oauth2_application_create_description": "Приложения OAuth2 предоставляет стороннему приложению доступ к учётным записям пользователей данного сервиса.", "settings.oauth2_application_remove_description": "Удаление приложения OAuth2 приведёт к отмене его доступа к авторизованным учётным записям пользователей в данном экземпляре. Продолжить?", "settings.authorized_oauth2_applications": "Авторизованные приложения OAuth2", "settings.revoke_key": "Отозвать", @@ -766,7 +755,6 @@ "repo.visibility_description": "Только владелец или члены организации, при наличии прав, смогут увидеть это.", "repo.visibility_helper": "Сделать репозиторий приватным", "repo.visibility_helper_forced": "Администратор сайта настроил параметр видимости новых репозиториев. Репозиторий приватный по умолчанию.", - "repo.visibility_fork_helper": "(Изменение этого повлияет на все форки.)", "repo.clone_helper": "Нужна помощь в клонировании? Посетите страницу помощи.", "repo.fork_repo": "Форкнуть репозиторий", "repo.fork_from": "Форк от", @@ -1033,7 +1021,6 @@ "repo.editor.upload_file_is_locked": "Файл «%s» заблокирован %s.", "repo.editor.upload_files_to_dir": "Загрузить файлы в «%s»", "repo.editor.cannot_commit_to_protected_branch": "Невозможно сделать коммит в защищённую ветку «%s».", - "repo.editor.no_commit_to_branch": "Невозможно совершить прямой коммит в ветку по причине:", "repo.editor.user_no_push_to_branch": "Пользователь не может отправлять коммиты в эту ветку", "repo.editor.require_signed_commit": "Ветка ожидает подписанный коммит", "repo.editor.cherry_pick": "Перенести изменения %s в:", @@ -1041,7 +1028,6 @@ "repo.commits.desc": "Просмотр истории изменений исходного кода.", "repo.commits.commits": "Коммитов", "repo.commits.no_commits": "Нет общих коммитов. «%s» и «%s» имеют совершенно разные истории.", - "repo.commits.nothing_to_compare": "Эти ветки одинаковы.", "repo.commits.search.tooltip": "Можно предварять ключевые слова префиксами \"author:\", \"committer:\", \"after:\", или \"before:\", например \"revert author:Alice before:2019-01-13\".", "repo.commits.search_all": "Все ветки", "repo.commits.author": "Автор", @@ -1109,11 +1095,9 @@ "repo.issues.new": "Новая задача", "repo.issues.new.title_empty": "Заголовок не может быть пустым", "repo.issues.new.labels": "Метки", - "repo.issues.new.no_label": "Нет меток", "repo.issues.new.clear_labels": "Очистить метки", "repo.issues.new.projects": "Проекты", "repo.issues.new.clear_projects": "Очистить проекты", - "repo.issues.new.no_projects": "Нет проекта", "repo.issues.new.open_projects": "Открытые проекты", "repo.issues.new.closed_projects": "Закрытые проекты", "repo.issues.new.no_items": "Нет элементов", @@ -1229,7 +1213,6 @@ "repo.issues.close": "Закрыть задачу", "repo.issues.comment_pull_merged_at": "слил(а) коммит %[1]s в %[2]s %[3]s", "repo.issues.comment_manually_pull_merged_at": "вручную слил(а) коммит %[1]s в %[2]s %[3]s", - "repo.issues.reopen_issue": "Открыть снова", "repo.issues.create_comment": "Комментировать", "repo.issues.closed_at": "закрыл(а) эту задачу %[2]s", "repo.issues.reopened_at": "переоткрыл(а) эту проблему %[2]s", @@ -1411,7 +1394,6 @@ "repo.pulls.allow_edits_from_maintainers": "Разрешить редактирование сопровождающими", "repo.pulls.allow_edits_from_maintainers_desc": "Пользователи с доступом на запись в основную ветку могут отправлять изменения и в эту ветку", "repo.pulls.allow_edits_from_maintainers_err": "Не удалось обновить", - "repo.pulls.compare_changes_desc": "Сравнить две ветки и создать запрос на слияние для изменений.", "repo.pulls.has_viewed_file": "Просмотрено", "repo.pulls.has_changed_since_last_review": "Изменено с момента вашего последнего отзыва", "repo.pulls.viewed_files_label": "%[1]d из %[2]d файлов просмотрено", @@ -1427,7 +1409,6 @@ "repo.pulls.showing_only_single_commit": "Показать только изменения коммита %[1]s", "repo.pulls.showing_specified_commit_range": "Показаны только изменения между %[1]s..%[2]", "repo.pulls.filter_changes_by_commit": "Фильтр по коммиту", - "repo.pulls.nothing_to_compare": "Нечего сравнивать, родительская и текущая ветка одинаковые.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "Ветки идентичны. Этот PR будет пустым.", "repo.pulls.has_pull_request": "Запрос на слияние этих веток уже существует: %[2]s#%[3]d", "repo.pulls.create": "Создать запрос на слияние", @@ -1486,7 +1467,6 @@ "repo.pulls.status_checking": "Выполняются некоторые проверки", "repo.pulls.status_checks_success": "Все проверки выполнены успешно", "repo.pulls.status_checks_warning": "Некоторые проверки сообщили о предупреждениях", - "repo.pulls.status_checks_failure": "Некоторые проверки не удались", "repo.pulls.status_checks_error": "Некоторые проверки сообщили об ошибках", "repo.pulls.status_checks_requested": "Требуется", "repo.pulls.status_checks_details": "Информация", @@ -1552,7 +1532,6 @@ "repo.signing.wont_sign.headsigned": "Слияние не будет подписано, так как головной коммит не подписан.", "repo.signing.wont_sign.commitssigned": "Слияние не будет подписано, так как все связанные коммиты не подписаны.", "repo.signing.wont_sign.approved": "Слияние не будет подписано, так как запрос на слияние не одобрен.", - "repo.signing.wont_sign.not_signed_in": "Вы не вошли в систему.", "repo.ext_wiki": "Доступ к внешней вики", "repo.ext_wiki.desc": "Ссылка на внешнюю вики.", "repo.wiki": "Вики", @@ -1745,7 +1724,6 @@ "repo.settings.transfer_abort_invalid": "Невозможно отменить трансфер несуществующего репозитория.", "repo.settings.transfer_abort_success": "Передача репозитория %s успешно отменена.", "repo.settings.transfer_desc": "Передать репозиторий другому пользователю или организации где у вас есть права администратора.", - "repo.settings.transfer_form_title": "Введите сопутствующую информацию для подтверждения операции:", "repo.settings.transfer_notices_1": "- Вы можете потерять доступ, если новый владелец является отдельным пользователем.", "repo.settings.transfer_notices_2": "- Вы сохраните доступ, если новым владельцем станет организация, владельцем которой вы являетесь.", "repo.settings.transfer_notices_3": "- если репозиторий является приватным и передается отдельному пользователю, это действие позволяет убедиться, что пользователь имеет хотя бы права на чтение (и при необходимости изменяет права доступа).", @@ -1807,8 +1785,6 @@ "repo.settings.webhook.delivery.success": "Событие было добавлено в очередь доставки. Может пройти несколько секунд, прежде чем оно отобразится в истории.", "repo.settings.githooks_desc": "Git-хуки предоставляются самим Git. Вы можете изменять файлы хуков из списка ниже, чтобы настроить собственные операции.", "repo.settings.githook_edit_desc": "Если хук не активен, будет подставлен пример содержимого. Пустое значение в этом поле приведёт к отключению хука.", - "repo.settings.githook_name": "Название хукa", - "repo.settings.githook_content": "Содержимое хука", "repo.settings.update_githook": "Обновить хук", "repo.settings.add_webhook_desc": "Gitea будет оправлять POST запросы на указанный URL адрес, с информацией о происходящих событиях. Подробности на странице инструкции по использованию веб-хуков.", "repo.settings.payload_url": "URL обработчика", @@ -1941,7 +1917,6 @@ "repo.settings.block_on_official_review_requests_desc": "Слияние невозможно, если не имеется достаточное количество одобрений официальных представителей.", "repo.settings.block_outdated_branch": "Блокировать слияние, если запрос на слияние устарел", "repo.settings.block_outdated_branch_desc": "Слияние будет невозможно, если головная ветвь находится позади базовой ветви.", - "repo.settings.default_branch_desc": "Главная ветка является \"базовой\" для вашего репозитория, на которую по умолчанию направлены все запросы на слияние и которая является лицом вашего репозитория. Первое, что увидит посетитель — это содержимое главной ветки. Выберите её из уже существующих:", "repo.settings.merge_style_desc": "Стили слияния", "repo.settings.choose_branch": "Выберите ветку…", "repo.settings.no_protected_branch": "Нет защищённых веток.", @@ -2030,8 +2005,6 @@ "repo.diff.too_many_files": "Некоторые файлы не были показаны из-за слишком большого количества измененных файлов", "repo.diff.show_more": "Показать больше", "repo.diff.load": "Загрузить разницу", - "repo.diff.generated": "сгенерированный", - "repo.diff.vendored": "поставляемый", "repo.diff.comment.placeholder": "Оставить комментарий", "repo.diff.comment.add_single_comment": "Добавить простой комментарий", "repo.diff.comment.add_review_comment": "Добавить комментарий", @@ -2046,6 +2019,7 @@ "repo.diff.review.reject": "Запрос изменений", "repo.diff.review.self_approve": "Авторы запросов на слияние не могут одобрять собственные запросы", "repo.diff.committed_by": "коммит произвёл", + "repo.commits.avatar_stack_and": "и", "repo.diff.protected": "Защищено", "repo.diff.image.side_by_side": "Режим \"рядом\"", "repo.diff.image.swipe": "Свайп", @@ -2118,7 +2092,6 @@ "repo.branch.restore_success": "Ветка «%s» восстановлена.", "repo.branch.restore_failed": "Не удалось восстановить ветку «%s».", "repo.branch.protected_deletion_failed": "Ветка «%s» защищена. Её нельзя удалить.", - "repo.branch.default_deletion_failed": "Ветка «%s» является веткой по умолчанию. Её нельзя удалить.", "repo.branch.restore": "Восстановить ветку «%s»", "repo.branch.download": "Скачать ветку «%s»", "repo.branch.rename": "Переименовать ветку «%s»", @@ -2195,9 +2168,7 @@ "org.settings.labels_desc": "Добавьте метки, которые могут быть использованы в задачах для всех репозиториев этой организации.", "org.members.membership_visibility": "Видимость участника команды:", "org.members.public": "Видимый", - "org.members.public_helper": "скрыть", "org.members.private": "Скрыт", - "org.members.private_helper": "сделать видимым", "org.members.member_role": "Роль участника:", "org.members.owner": "Владелец", "org.members.member": "Участник", @@ -2227,7 +2198,6 @@ "org.teams.members": "Члены группы разработки", "org.teams.update_settings": "Обновить настройки", "org.teams.delete_team": "Удалить команду", - "org.teams.add_team_member": "Добавление члена группы разработки", "org.teams.invite_team_member": "Пригласить в %s", "org.teams.invite_team_member.list": "Приглашения в ожидании", "org.teams.delete_team_title": "Удалить команду", @@ -2252,6 +2222,10 @@ "org.teams.all_repositories_read_permission_desc": "Эта команда предоставляет прочтено доступ к всем репозиториям: участники могут просматривать и клонировать репозитории.", "org.teams.all_repositories_write_permission_desc": "Эта команда предоставляет Написать доступ к всем репозиториям: участники могут читать и выполнять push в репозитории.", "org.teams.all_repositories_admin_permission_desc": "Эта команда предоставляет администратору доступ к всем репозиториям: участники могут читать, отправлять сообщения и добавлять соавторов в репозитории.", + "org.teams.visibility": "Видимость", + "org.teams.visibility_private": "Личный", + "org.teams.visibility_limited": "Ограниченный", + "org.teams.visibility_public": "Публичный", "org.teams.invite.title": "Вас пригласили присоединиться к команде %s организации %s.", "org.teams.invite.by": "Приглашен(а) %s", "org.teams.invite.description": "Нажмите на кнопку ниже, чтобы присоединиться к команде.", @@ -2263,6 +2237,7 @@ "admin.hooks": "Веб-хуки", "admin.integrations": "Интеграции", "admin.authentication": "Аутентификация", + "admin.badges.description": "Описание", "admin.config": "Конфигурация", "admin.config_summary": "Статистика", "admin.config_settings": "Настройки", @@ -2525,11 +2500,8 @@ "admin.config.server_config": "Конфигурация сервера", "admin.config.app_name": "Название сайта", "admin.config.app_ver": "Версия Gitea", - "admin.config.app_url": "Базовый URL Gitea", "admin.config.custom_conf": "Путь к файлу конфигурации", "admin.config.custom_file_root_path": "Пользовательский путь до папки с файлами", - "admin.config.domain": "Домен сервера", - "admin.config.offline_mode": "Локальный режим", "admin.config.disable_router_log": "Отключение журнала маршрутизатора", "admin.config.run_user": "Запуск от имени пользователя", "admin.config.run_mode": "Режим выполнения", @@ -2579,7 +2551,6 @@ "admin.config.default_enable_timetracking": "Включить отслеживание времени по умолчанию", "admin.config.default_allow_only_contributors_to_track_time": "Учитывать только участников разработки в подсчёте времени", "admin.config.no_reply_address": "No-reply адрес", - "admin.config.default_visibility_organization": "Видимость по умолчанию для новых организаций", "admin.config.default_enable_dependencies": "Включение зависимостей для задач по умолчанию", "admin.config.webhook_config": "Конфигурация веб-хуков", "admin.config.queue_length": "Длина очереди", @@ -2607,11 +2578,9 @@ "admin.config.cache_config": "Настройки кеша", "admin.config.cache_adapter": "Адаптер кэша", "admin.config.cache_interval": "Интервал кэширования", - "admin.config.cache_conn": "Подключение кэша", "admin.config.cache_item_ttl": "Время жизни данных в кеше", "admin.config.session_config": "Конфигурация сессии", "admin.config.session_provider": "Провайдер сессии", - "admin.config.provider_config": "Конфигурация провайдера", "admin.config.cookie_name": "Имя файла cookie", "admin.config.gc_interval_time": "Интервал работы сборщика мусора", "admin.config.session_life_time": "Время жизни сессии", @@ -2619,7 +2588,6 @@ "admin.config.cookie_life_time": "Время жизни файла cookie", "admin.config.picture_config": "Настройка изображения", "admin.config.picture_service": "Сервис изображений", - "admin.config.disable_gravatar": "Отключить Gravatar", "admin.config.enable_federated_avatar": "Включить внешние аватары", "admin.config.git_config": "Конфигурация Git", "admin.config.git_disable_diff_highlight": "Отключить подсветку синтаксиса при сравнении", @@ -2629,8 +2597,6 @@ "admin.config.git_gc_args": "Аргументы GC", "admin.config.git_migrate_timeout": "Лимит времени миграций", "admin.config.git_mirror_timeout": "Лимит времени обновления зеркал", - "admin.config.git_clone_timeout": "Лимит времени операции клонирования", - "admin.config.git_pull_timeout": "Лимит времени получения изменений", "admin.config.git_gc_timeout": "Лимит времени сборки мусора", "admin.config.log_config": "Конфигурация журнала", "admin.config.logger_name_fmt": "Журнал: %s", @@ -2849,6 +2815,10 @@ "packages.rubygems.required.rubygems": "Требуется версия RubyGem", "packages.swift.install": "Добавьте пакет в свой файл Package.swift:", "packages.swift.install2": "и запустите следующую команду:", + "packages.terraform.install2": "и запустите следующую команду:", + "packages.terraform.lock_status": "Смена статуса ограничения на обсуждение", + "packages.terraform.lock": "Ограничить", + "packages.terraform.unlock": "Снять", "packages.vagrant.install": "Чтобы добавить бокс Vagrant, выполните следующую команду:", "packages.settings.link": "Связать этот пакет с репозиторием", "packages.settings.link.select": "Выберите репозиторий", @@ -2951,15 +2921,21 @@ "actions.runs.actor": "Актор", "actions.runs.status": "Статус", "actions.runs.actors_no_select": "Все акторы", + "actions.runs.branch": "Операции с ветками", + "actions.runs.branches_no_select": "Все ветки", "actions.runs.no_results": "Ничего не найдено.", "actions.runs.no_workflows": "Пока нет рабочих процессов.", "actions.runs.no_runs": "Рабочий поток ещё не запускался.", "actions.runs.empty_commit_message": "(пустое сообщение коммита)", + "actions.runs.summary": "Статистика", "actions.workflow.disable": "Выключить рабочий поток", "actions.workflow.disable_success": "Рабочий поток «%s» успешно выключен.", "actions.workflow.enable": "Включить рабочий поток", "actions.workflow.enable_success": "Рабочий поток «%s» успешно включен.", "actions.workflow.disabled": "Рабочий поток выключен.", + "actions.workflow.scope_owner": "Владелец", + "actions.workflow.scope_global": "Глобально", + "actions.workflow.required": "Требуется", "actions.need_approval_desc": "Требуется одобрение, чтобы запустить рабочие потоки для запроса на слияние.", "actions.variables": "Переменные", "actions.variables.management": "Управление переменными", @@ -2979,8 +2955,6 @@ "projects.type-2.display_name": "Проект репозитория", "projects.type-3.display_name": "Проект организации", "git.filemode.directory": "Каталог", - "git.filemode.normal_file": "Обычный файл", - "git.filemode.executable_file": "Исполняемый файл", - "git.filemode.symbolic_link": "Символическая ссылка", - "git.filemode.submodule": "Подмодуль" + "git.filemode.submodule": "Подмодуль", + "actions.general.token_permissions.mode.restricted": "Ограничено" } diff --git a/options/locale/locale_si-LK.json b/options/locale/locale_si-LK.json index 12938a8eb1..316aedb8d7 100644 --- a/options/locale/locale_si-LK.json +++ b/options/locale/locale_si-LK.json @@ -137,12 +137,6 @@ "install.register_confirm": "ලියාපදිංචි වීමට විද්යුත් තැපැල් තහවුරු කිරීම අවශ්ය වේ", "install.mail_notify": "වි-තැපැල් දැනුම්දීම් සබල කරන්න", "install.server_service_title": "සේවාදායකය සහ තෙවන පාර්ශවීය සේවා සැකසුම්", - "install.offline_mode": "දේශීය ප්රකාරය සක්රීය කරන්න", - "install.offline_mode_popup": "තෙවන පාර්ශවීය අන්තර්ගත බෙදාහැරීමේ ජාල අක්රීය කර දේශීයව සියලු සම්පත් සේවය කරන්න.", - "install.disable_gravatar": "ග්‍රැවටාර් අබල කරන්න", - "install.disable_gravatar_popup": "Gravatar සහ තෙවන පාර්ශවීය avatar ප්රභවයන් අක්රීය කරන්න. පරිශීලකයෙකු දේශීයව අවතාරයක් උඩුගත නොකරන්නේ නම් පෙරනිමි අවතාරයක් භාවිතා කරනු ඇත.", - "install.federated_avatar_lookup": "ෆෙඩරල් අවතාර් සක්රීය කරන්න", - "install.federated_avatar_lookup_popup": "Libravatar භාවිතා ෆෙඩරල් අවතාර් විමසිම සක්රීය කරන්න.", "install.disable_registration": "ස්වයං ලියාපදිංචිය අක්රීය කරන්න", "install.disable_registration_popup": "පරිශීලක ස්වයං ලියාපදිංචිය අක්රීය කරන්න. නව පරිශීලක ගිණුම් නිර්මාණය කිරීමට හැක්කේ පරිපාලකයින්ට පමණි.", "install.allow_only_external_registration_popup": "විදේශ සේවා මගින් පමණක් ලියාපදිංචි වීමට ඉඩ දෙන්න", @@ -161,10 +155,8 @@ "install.admin_email": "වි-තැපැල් ලිපිනය", "install.install_btn_confirm": "ගිටියා ස්ථාපනය කරන්න", "install.test_git_failed": "'git' විධානය පරීක්ෂා කළ නොහැකි විය: %v", - "install.sqlite3_not_available": "මෙම Gitea අනුවාදය SQLite3 සඳහා සහය නොදක්වයි. කරුණාකර නිල ද්විමය අනුවාදය %s වෙතින් බාගත කරන්න ('gobuild' අනුවාදය නොවේ).", "install.invalid_db_setting": "දත්ත සමුදා සැකසුම් අවලංගුයි: %v", "install.invalid_repo_path": "නිධි මූල මාර්ගය අවලංගුයි: %v", - "install.run_user_not_match": "'ලෙස ධාවනය කරන්න' පරිශීලක නාමය වත්මන් පරිශීලක නාමය නොවේ: %s -> %s", "install.save_config_failed": "වින්යාසය සුරැකීමට අසමත් විය: %v", "install.invalid_admin_setting": "පරිපාලක ගිණුම් සැකසුම අවලංගුයි: %v", "install.invalid_log_root_path": "ලොග් මාර්ගය අවලංගුයි: %v", @@ -526,7 +518,6 @@ "settings.permission_read": "කියවූ", "settings.manage_oauth2_applications": "OUTU2 යෙදුම් කළමනාකරණය කරන්න", "settings.edit_oauth2_application": "OUT2 අයදුම්පත සංස්කරණය කරන්න", - "settings.oauth2_applications_desc": "OUTU2 අයදුම්පත් ආරක්ෂිතව මෙම Gitea උදාහරණයක් පරිශීලකයන් සත්යාපනය කිරීමට ඔබගේ තෙවන පාර්ශවීය අයදුම් හැකියාව ලබා දෙයි.", "settings.remove_oauth2_application": "Oauth2 අයදුම්පත ඉවත් කරන්න", "settings.remove_oauth2_application_desc": "OUT2 යෙදුමක් ඉවත් කිරීමෙන් අත්සන් කරන ලද සියලුම ප්රවේශ ටෝකන වෙත ප්රවේශය අවලංගු වේ. දිගටම?", "settings.remove_oauth2_application_success": "අයදුම්පත මකා දමා ඇත.", @@ -539,7 +530,6 @@ "settings.oauth2_regenerate_secret": "රහස් පුනර්ජනනය", "settings.oauth2_regenerate_secret_hint": "ඔබගේ රහස නැති වුනාද?", "settings.oauth2_application_edit": "සංස්කරණය", - "settings.oauth2_application_create_description": "OUTU2 යෙදුම් මෙම අවස්ථාවේ දී පරිශීලක ගිණුම් වෙත ඔබගේ තෙවන පාර්ශවීය අයදුම් ප්රවේශය ලබා දෙයි.", "settings.authorized_oauth2_applications": "බලයලත් OUTU2", "settings.revoke_key": "අවලංගු", "settings.revoke_oauth2_grant": "ප්රවේශ අවලංගු", @@ -586,7 +576,6 @@ "repo.visibility": "දෘශ්යතාව", "repo.visibility_description": "හිමිකරුට හෝ සංවිධානයේ සාමාජිකයින්ට අයිතිවාසිකම් තිබේ නම් එය දැක ගත හැකිය.", "repo.visibility_helper_forced": "ඔබේ වෙබ් අඩවි පරිපාලක නව ගබඩාවන් පුද්ගලික වීමට බල කරයි.", - "repo.visibility_fork_helper": "(මෙය වෙනස් කිරීම සියලු දෙබලක වලට බලපානු ඇත.)", "repo.clone_helper": "උදව් ක්ලෝනකරණය අවශ්යද? සංචාරය උදව්.", "repo.fork_repo": "දෙබලක ගබඩාව", "repo.fork_from": "සිට දෙබලක", @@ -784,12 +773,10 @@ "repo.editor.no_changes_to_show": "පෙන්වීමට කිසිදු වෙනසක් නැත.", "repo.editor.push_rejected_summary": "පූර්ණ ප්රතික්ෂේප පණිවිඩය:", "repo.editor.add_subdir": "ඩිරෙක්ටරියක් එක් කරන්න…", - "repo.editor.no_commit_to_branch": "ශාඛාවට කෙලින්ම කැපවිය නොහැකි නිසා:", "repo.editor.user_no_push_to_branch": "පරිශීලකයාට ශාඛාවට තල්ලු කළ නොහැක", "repo.editor.require_signed_commit": "ශාඛාවට අත්සන් කළ කැපවීමක් අවශ්ය වේ", "repo.commits.desc": "මූලාශ්ර කේත වෙනස් කිරීමේ ඉතිහාසය පිරික්සන්න.", "repo.commits.commits": "විවරයන්", - "repo.commits.nothing_to_compare": "මෙම ශාඛා සමාන වේ.", "repo.commits.search_all": "සියළුම ශාඛා", "repo.commits.author": "කතෘ", "repo.commits.message": "පණිවිඩය", @@ -834,11 +821,9 @@ "repo.issues.new": "නව නිකුතුව", "repo.issues.new.title_empty": "මාතෘකාව හිස් විය නොහැක", "repo.issues.new.labels": "ලේබල", - "repo.issues.new.no_label": "ලේබලයක් නැත", "repo.issues.new.clear_labels": "ලේබල පැහැදිලි", "repo.issues.new.projects": "ව්‍යාපෘති", "repo.issues.new.clear_projects": "පැහැදිලි ව්යාපෘති", - "repo.issues.new.no_projects": "ව්‍යාපෘති නැත", "repo.issues.new.open_projects": "විවෘත ව්‍යාපෘති", "repo.issues.new.closed_projects": "සංවෘත ව්‍යාපෘති", "repo.issues.new.milestone": "සන්ධිස්ථානය", @@ -923,7 +908,6 @@ "repo.issues.context.reference_issue": "නව නිකුතුවක යොමු කිරීම", "repo.issues.context.edit": "සංස්කරණය", "repo.issues.context.delete": "මකන්න", - "repo.issues.reopen_issue": "නැවත විවෘත කරන්න", "repo.issues.create_comment": "අදහස", "repo.issues.closed_at": "මෙම ගැටළුව වසා %[2]s", "repo.issues.reopened_at": "මෙම ගැටළුව නැවත විවෘත කරන ලදි %[2]s", @@ -1068,13 +1052,11 @@ "repo.pulls.desc": "අදින්න ඉල්ලීම් සහ කේත සමාලෝචන සක්රීය කරන්න.", "repo.pulls.new": "නව අදින්න ඉල්ලීම", "repo.pulls.compare_changes": "නව අදින්න ඉල්ලීම", - "repo.pulls.compare_changes_desc": "ඒකාබද්ධ කිරීම සඳහා ශාඛාව සහ ශාඛාව අදින්න.", "repo.pulls.compare_base": "ඒකාබද්ධ", "repo.pulls.compare_compare": "සිට අදින්න", "repo.pulls.switch_comparison_type": "ස්විච් සංසන්දනය වර්ගය", "repo.pulls.switch_head_and_base": "හිස සහ පාදය මාරු කරන්න", "repo.pulls.filter_branch": "ශාඛාව පෙරන්න", - "repo.pulls.nothing_to_compare": "මෙම ශාඛා සමාන වේ. අදින්න ඉල්ලීමක් නිර්මාණය කිරීමට අවශ්ය නැත.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "මෙම ශාඛා සමාන වේ. මෙම මහජන සම්බන්ධතා හිස් වනු ඇත.", "repo.pulls.has_pull_request": "මෙම ශාඛා අතර අදින්න ඉල්ලීම දැනටමත් පවතී: %[2]s #%[3]d", "repo.pulls.create": "අදින්න ඉල්ලීම නිර්මාණය", @@ -1127,7 +1109,6 @@ "repo.pulls.status_checking": "සමහර චෙක්පත් බලා ඇත", "repo.pulls.status_checks_success": "සියලුම චෙක්පත් සාර්ථක විය", "repo.pulls.status_checks_warning": "සමහර චෙක්පත් අනතුරු ඇඟවීම් වාර්තා කරයි", - "repo.pulls.status_checks_failure": "සමහර චෙක්පත් අසමත් විය", "repo.pulls.status_checks_error": "සමහර චෙක්පත් දෝෂ වාර්තා කරයි", "repo.pulls.status_checks_requested": "ඇවැසිය", "repo.pulls.status_checks_details": "වැණුම", @@ -1316,7 +1297,6 @@ "repo.settings.transfer_abort": "මාරු කිරීම අවලංගු කරන්න", "repo.settings.transfer_abort_invalid": "ඔබට නොපවතින නිධි හුවමාරුවක් අවලංගු කළ නොහැක.", "repo.settings.transfer_desc": "මෙම ගබඩාව පරිශීලකයෙකුට හෝ ඔබට පරිපාලක අයිතිවාසිකම් ඇති සංවිධානයකට මාරු කරන්න.", - "repo.settings.transfer_form_title": "තහවුරු කිරීමක් ලෙස නිධි නාමය ඇතුලත් කරන්න:", "repo.settings.transfer_notices_1": "- ඔබ එය තනි පරිශීලකයෙකුට මාරු කළහොත් ඔබට ගබඩාවට ප්රවේශය අහිමි වනු ඇත.", "repo.settings.transfer_notices_2": "- ඔබ එය ඔබට (co-) අයිති සංවිධානයක් වෙත මාරු කළහොත් ඔබ ගබඩාවට ප්රවේශය තබා ගනු ඇත.", "repo.settings.transfer_notices_3": "- ගබඩාව පුද්ගලික වන අතර එය තනි පරිශීලකයෙකුට මාරු කරනු ලැබේ නම්, මෙම ක්රියාව පරිශීලකයාට අවම වශයෙන් කියවීමට අවසර ලබා ඇති බවට වග බලා ගනී (සහ අවශ්ය නම් අවසර වෙනස් කරයි).", @@ -1373,8 +1353,6 @@ "repo.settings.webhook.payload": "අන්තර්ගතය", "repo.settings.webhook.body": "ශරීරය", "repo.settings.githook_edit_desc": "කොක්කෙන් අක්රිය නම්, නියැදි අන්තර්ගතය ඉදිරිපත් කරනු ලැබේ. හිස් අගය කිරීමට අන්තර්ගතය පිටත් මෙම කොක්කෙන් අක්රීය කරනු ඇත.", - "repo.settings.githook_name": "කොක්කෙන් නම", - "repo.settings.githook_content": "කොක්කෙන් අන්තර්ගත", "repo.settings.update_githook": "යාවත්කාලීන හූක්", "repo.settings.add_webhook_desc": "Gitea විසින් නිශ්චිත අන්තර්ගත වර්ගයක් සහිත පොස්ට් ඉල්ලීම් ඉලක්කගත URL වෙත යවනු ලැබේ. තව දුරටත් කියවන්න වෙබ් කොකු මාර්ගෝපදේශය.", "repo.settings.payload_url": "ඉලක්ක URL", @@ -1479,7 +1457,6 @@ "repo.settings.block_on_official_review_requests_desc": "නිල සමාලෝචන ඉල්ලීම් ඇති විට ඒකාබද්ධ කිරීම කළ නොහැකි වනු ඇත, ප්රමාණවත් අනුමැතීන් තිබුණත්.", "repo.settings.block_outdated_branch": "අදින්න ඉල්ලීම යල් පැන ගිය නම් වාරණ ඒකාබද්ධ", "repo.settings.block_outdated_branch_desc": "ප්රධාන ශාඛාව මූලික ශාඛාව පිටුපස ඇති විට ඒකාබද්ධ කිරීම කළ නොහැකි වනු ඇත.", - "repo.settings.default_branch_desc": "අදින්න ඉල්ලීම් සහ කේත විවරණය සඳහා පෙරනිමි ගබඩාවේ ශාඛාවක් තෝරන්න:", "repo.settings.choose_branch": "ශාඛාවක් තෝරන්න…", "repo.settings.no_protected_branch": "ආරක්ෂිත ශාඛා නොමැත.", "repo.settings.edit_protected_branch": "සංස්කරණය", @@ -1563,8 +1540,6 @@ "repo.diff.too_many_files": "බොහෝ ගොනු මෙම diff වෙනස් වී ඇති නිසා සමහර ගොනු පෙන්වා නැත", "repo.diff.show_more": "තව පෙන්වන්න", "repo.diff.load": "බර අඩු", - "repo.diff.generated": "ජනනය", - "repo.diff.vendored": "අලෙවි", "repo.diff.comment.placeholder": "අදහසක් හැරයන්න", "repo.diff.comment.add_single_comment": "තනි අදහස් එක් කරන්න", "repo.diff.comment.add_review_comment": "අදහස එකතු කරන්න", @@ -1577,6 +1552,7 @@ "repo.diff.review.approve": "අනුමත", "repo.diff.review.reject": "වෙනස්කම් ඉල්ලන්න", "repo.diff.committed_by": "විසින් කැප", + "repo.commits.avatar_stack_and": "සහ", "repo.diff.protected": "ආරක්ෂිත", "repo.diff.image.side_by_side": "පැත්ත විසින් පැත්ත", "repo.diff.image.swipe": "ස්වයිප්", @@ -1683,9 +1659,7 @@ "org.settings.labels_desc": "මෙම සංවිධානය යටතේ සියලුම ගබඩාවලදී සඳහා ගැටළු සඳහා භාවිතා කළ හැකි ලේබල් එකතු කරන්න.", "org.members.membership_visibility": "සාමාජිකත්ව දෘශ්යතාව:", "org.members.public": "දෘශ්යමාන", - "org.members.public_helper": "සැඟවුනු කරන්න", "org.members.private": "සැඟවුනු", - "org.members.private_helper": "දෘශ්යමාන කරන්න", "org.members.member_role": "සාමාජික කාර්යභාරය:", "org.members.owner": "හිමිකරු", "org.members.member": "සාමාජික", @@ -1712,7 +1686,6 @@ "org.teams.members": "කණ්ඩායමේ සාමාජිකයින්", "org.teams.update_settings": "සැකසුම් යාවත්කාල කරන්න", "org.teams.delete_team": "කණ්ඩායම මකන්න", - "org.teams.add_team_member": "කණ්ඩායම් සාමාජික එකතු කරන්න", "org.teams.delete_team_title": "කණ්ඩායම මකන්න", "org.teams.delete_team_desc": "කණ්ඩායමක් මකා දැමීම එහි සාමාජිකයන්ගෙන් ගබඩාව ප්රවේශය අවලංගු කරයි. දිගටම?", "org.teams.delete_team_success": "කණ්ඩායම මකා දමා ඇත.", @@ -1735,12 +1708,17 @@ "org.teams.all_repositories_read_permission_desc": "මෙම කණ්ඩායම ප්රදානය කියවන්න ප්රවේශ සියලු ගබඩාවන්ට: සාමාජිකයින්ට නිධි බැලීම හා පරිගණක ක්රිඩාවට සමාන කළ හැකිය.", "org.teams.all_repositories_write_permission_desc": "මෙම කණ්ඩායම ප්රදානය කරයි වෙත ප්රවේශය ලියන්න සියලු ගබඩාවන්ට: සාමාජිකයින්ට කියවීමට සහ ගබඩාවන්ට තල්ලු කළ හැකිය.", "org.teams.all_repositories_admin_permission_desc": "මෙම කණ්ඩායම ප්රදානය කරයි පරිපාලක වෙත ප්රවේශය සියලු ගබඩාවන්ට: සාමාජිකයින්ට කියවීමට, තල්ලු කිරීමට සහ ගබඩාවන්ට සහයෝගීකයින් එකතු කිරීමට.", + "org.teams.visibility": "දෘශ්යතාව", + "org.teams.visibility_private": "පෞද්ගලික", + "org.teams.visibility_limited": "සීමිත", + "org.teams.visibility_public": "ප්‍රසිද්ධ", "admin.dashboard": "උපකරණ පුවරුව", "admin.users": "පරිශීලක ගිණුම්", "admin.organizations": "සංවිධාන", "admin.repositories": "කෝෂ්ඨ", "admin.hooks": "වෙබ්කොකු", "admin.authentication": "සත්යාපන ප්රභවයන්", + "admin.badges.description": "සවිස්තරය", "admin.config": "වින්‍යාසය", "admin.config_summary": "සාරාංශය", "admin.config_settings": "සැකසුම්", @@ -1971,11 +1949,8 @@ "admin.config.server_config": "සේවාදායකයේ වින්‍යාසය", "admin.config.app_name": "අඩවියේ සිරැසිය", "admin.config.app_ver": "ගිටියා අනුවාදය", - "admin.config.app_url": "මූලික URL එක කරන්න", "admin.config.custom_conf": "වින්යාස ගොනුවේ මාර්ගය", "admin.config.custom_file_root_path": "අභිරුචි ගොනු රූට් මාර්ගය", - "admin.config.domain": "සේවාදායකයේ වසම", - "admin.config.offline_mode": "දේශීය ප්රකාරය", "admin.config.disable_router_log": "Router ලොග් අක්රීය", "admin.config.run_user": "පරිශීලක නාමය ලෙස ධාවනය", "admin.config.run_mode": "ධාවනය ප්රකාරය", @@ -2024,7 +1999,6 @@ "admin.config.default_enable_timetracking": "පෙරනිමියෙන් කාල ට්රැකින් සක්රීය කරන්න", "admin.config.default_allow_only_contributors_to_track_time": "දායකයින්ට පමණක් වේලාව නිරීක්ෂණය කිරීමට ඉඩ දෙන්න", "admin.config.no_reply_address": "සැඟවුණු ඊ-තැපැල් වසම", - "admin.config.default_visibility_organization": "නව සංවිධාන සඳහා පෙරනිමි දෘශ්යතාව", "admin.config.default_enable_dependencies": "පෙරනිමියෙන් නිකුත් යැපීම් සක්රීය", "admin.config.webhook_config": "වෙබ්හූක් වින්යාසය", "admin.config.queue_length": "පෝලිම් දිග", @@ -2044,11 +2018,9 @@ "admin.config.cache_config": "නිහිතයේ වින්‍යාසය", "admin.config.cache_adapter": "හැඹිලි ඇඩප්ටර", "admin.config.cache_interval": "හැඹිලි පරතරය", - "admin.config.cache_conn": "නිහිතයේ සම්බන්ධතාවය", "admin.config.cache_item_ttl": "හැඹිලි අයිතමය TTL", "admin.config.session_config": "සැසිය වින්යාසය", "admin.config.session_provider": "සැසිය සැපයුම්කරු", - "admin.config.provider_config": "සපයන්නා වින්යාස", "admin.config.cookie_name": "කුකී නම", "admin.config.gc_interval_time": "GC පරතරය වේලාව", "admin.config.session_life_time": "සැසිය ජීවිත කාලය", @@ -2056,7 +2028,6 @@ "admin.config.cookie_life_time": "කුකී ජීවිත කාලය", "admin.config.picture_config": "පින්තූරය සහ අවතාර් වින්යාසය", "admin.config.picture_service": "පින්තුරය සේවා", - "admin.config.disable_gravatar": "ග්‍රැවටාර් අබල කරන්න", "admin.config.enable_federated_avatar": "ෆෙඩරල් අවතාර් සක්රීය කරන්න", "admin.config.git_config": "ගිට් වින්‍යාසය", "admin.config.git_disable_diff_highlight": "ඩිෆ් සයින්ටැක්ස් ඉස්මතු කරන්න අක්රීය කරන්න", @@ -2066,8 +2037,6 @@ "admin.config.git_gc_args": "GC තර්ක", "admin.config.git_migrate_timeout": "සංක්රමණ කාලය", "admin.config.git_mirror_timeout": "මිරර් යාවත්කාලීන වේලාව", - "admin.config.git_clone_timeout": "පරිගණක ක්රිඩාවට සමාන මෙහෙයුම් කාලය", - "admin.config.git_pull_timeout": "මෙහෙයුම කාල ඉමුද අදින්න", "admin.config.git_gc_timeout": "GC මෙහෙයුම කාලය", "admin.config.log_config": "ලොග් වින්යාසය", "admin.config.disabled_logger": "අබල කර ඇත", @@ -2177,6 +2146,8 @@ "packages.container.labels": "ලේබල", "packages.npm.dependencies": "පරායත්ත", "packages.npm.details.tag": "ටැග", + "packages.terraform.lock": "අගුළු ලන්න", + "packages.terraform.unlock": "අගුළු හරින්න", "packages.owner.settings.cleanuprules.enabled": "සබල කර ඇත", "secrets.creation.description": "සවිස්තරය", "actions.runners.name": "නම", @@ -2189,5 +2160,8 @@ "actions.runners.status.active": "ක්රියාකාරී", "actions.runners.version": "අනුවාදය", "actions.runs.commit": "කැප", - "git.filemode.symbolic_link": "සංකේතාත්මක සබැඳිය" + "actions.runs.summary": "සාරාංශය", + "actions.workflow.scope_owner": "හිමිකරු", + "actions.workflow.required": "ඇවැසිය", + "actions.general.token_permissions.mode.restricted": "සීමා" } diff --git a/options/locale/locale_sk-SK.json b/options/locale/locale_sk-SK.json index 908abec839..263c4180bf 100644 --- a/options/locale/locale_sk-SK.json +++ b/options/locale/locale_sk-SK.json @@ -198,12 +198,6 @@ "install.register_confirm": "Registrácia vyžaduje potvrdenie e-mailu", "install.mail_notify": "Povoliť e-mailové upozornenia", "install.server_service_title": "Nastavenia servera a ostatných služieb", - "install.offline_mode": "Povoliť miestny režim", - "install.offline_mode_popup": "Zakázať siete doručovania obsahu tretích strán a poskytovať celý obsah lokálne.", - "install.disable_gravatar": "Zakázať Gravatar", - "install.disable_gravatar_popup": "Zakázať Gravatar a cudzie zdroje avatarov. Ak používateľ nenahrá avatara, použije sa predvolený.", - "install.federated_avatar_lookup": "Povoliť avatary z verejných zdrojov", - "install.federated_avatar_lookup_popup": "Povoliť Libavatar na vyhľadávanie avatarov z verejných zdrojov.", "install.disable_registration": "Zakázať registráciu", "install.disable_registration_popup": "Zakázať registráciu. Nové používateľské účty budú môcť vytvárať iba správci.", "install.allow_only_external_registration_popup": "Povoliť registráciu iba skrze externé služby", @@ -223,11 +217,9 @@ "install.admin_email": "E-mailová adresa", "install.install_btn_confirm": "Nainštalovať Gitea", "install.test_git_failed": "Nie je možné otestovať príkaz 'git': %v", - "install.sqlite3_not_available": "Táto verzia Gitea nepodporuje SQLite3. Stiahnite si, prosím, oficiálnu verziu z %s (nie verziu \"gobuild\").", "install.invalid_db_setting": "Nastavenia databázy sú neplatné: %v", "install.invalid_repo_path": "Koreňová cesta repozitára je neplatná: %v", "install.invalid_app_data_path": "Cesta k údajom aplikácie je neplatná: %v", - "install.run_user_not_match": "Používateľské meno pre 'spustiť ako' nie je aktuálne používateľské meno: %s -> %s", "install.internal_token_failed": "Nepodarilo sa vygenerovať interný token: %v", "install.secret_key_failed": "Nepodarilo sa vygenerovať tajný kľúč: %v", "install.save_config_failed": "Nepodarilo sa uložiť konfiguráciu: %v", @@ -624,7 +616,6 @@ "settings.permission_read": "Čítanie", "settings.manage_oauth2_applications": "Správa OAuth2 aplikácií", "settings.edit_oauth2_application": "Upraviť aplikáciu OAuth2", - "settings.oauth2_applications_desc": "OAuth2 aplikácie umožňujú aplikáciám tretej strany bezpečne autentifikovať používateľov v tejto inštancii Gitea.", "settings.remove_oauth2_application": "Odstrániť aplikáciu OAuth2", "settings.remove_oauth2_application_desc": "Odstránenie aplikácie OAuth2 zruší prístup všetkým podpísaným prístupovým tokenom. Ďalej?", "settings.remove_oauth2_application_success": "Aplikácia bola odstránená.", @@ -637,7 +628,6 @@ "settings.oauth2_regenerate_secret": "Obnoviť tajný kľúč", "settings.oauth2_regenerate_secret_hint": "Stratili ste svoj tajný kľúč?", "settings.oauth2_application_edit": "Upraviť", - "settings.oauth2_application_create_description": "Aplikácie OAuth2 poskytujú aplikáciám tretích strán prístup k používateľským účtom v tejto inštancii.", "settings.authorized_oauth2_applications": "Autorizované aplikácie OAuth2", "settings.revoke_key": "Odvolať", "settings.revoke_oauth2_grant": "Odstrániť prístup", @@ -687,7 +677,6 @@ "repo.visibility": "Viditeľnosť", "repo.visibility_description": "Vidieť ho bude iba vlastník alebo členovia organizácie, ak majú práva.", "repo.visibility_helper_forced": "Váš správca vynucuje že nové repozitáre musia byť súkromné.", - "repo.visibility_fork_helper": "(Zmena ovplyvní všetky forky.)", "repo.clone_helper": "Potrebujete pomoc s klonovaním? Navštívte Pomocníka.", "repo.use_template": "Použiť túto šablónu", "repo.generate_repo": "Generovať repozitár", @@ -844,7 +833,6 @@ "repo.editor.cancel": "Zrušiť", "repo.editor.commit_empty_file_header": "Odoslať prázdny súbor", "repo.editor.commit_empty_file_text": "Súbor, ktorý sa chystáte odoslať, je prázdny. Pokračovať?", - "repo.editor.no_commit_to_branch": "Nedá sa odoslať priamo do vetvy, pretože:", "repo.editor.require_signed_commit": "Vetva vyžaduje podpísaný commit", "repo.commits.commits": "Commity", "repo.commits.search_all": "Všetky vetvy", @@ -1044,7 +1032,6 @@ "org.settings.confirm_delete_account": "Potvrdiť odstránenie", "org.settings.hooks_desc": "Pridajte webhooky, ktoré sa spustia nad všetkými repozitármi v rámci tejto organizácie.", "org.members.private": "Skrytý", - "org.members.private_helper": "zviditeľniť", "org.members.owner": "Vlastník", "org.members.remove": "Odstrániť", "org.teams.read_access": "Čítanie", @@ -1059,10 +1046,14 @@ "org.teams.all_repositories_read_permission_desc": "Tomuto tímu je pridelený prístup na čítanie ku všetkým repozitárom: členovia môžu prezerať a klonovať repozitáre.", "org.teams.all_repositories_write_permission_desc": "Tomuto tímu je pridelený prístup na Zápis do všetkých repozitárov: členovia môžu prezerať a nahrávať do repozitárov.", "org.teams.all_repositories_admin_permission_desc": "Tomuto tímu je pridelený Admin prístup ku všetkým repozitárom: členovia môžu prezerať, nahrávať do repozitárov a pridávať do nich spolupracovníkov.", + "org.teams.visibility": "Viditeľnosť", + "org.teams.visibility_private": "Súkromný", + "org.teams.visibility_limited": "Obmedzený", "admin.dashboard": "Nástenka", "admin.organizations": "Organizácie", "admin.repositories": "Repozitáre", "admin.hooks": "Webhooky", + "admin.badges.description": "Popis", "admin.config_settings": "Nastavenia", "admin.dashboard.delete_generated_repository_avatars": "Odstrániť vygenerované avatary repozitárov", "admin.users.name": "Používateľské meno", @@ -1095,8 +1086,6 @@ "admin.auths.sspi_default_language": "Predvolený jazyk používateľa", "admin.config.app_name": "Názov webu", "admin.config.app_ver": "Verzia Gitea", - "admin.config.app_url": "Základná URL Gitea", - "admin.config.domain": "Doména serveru", "admin.config.run_user": "Spustiť ako používateľ", "admin.config.repo_root_path": "Koreňový adresár repozitárov", "admin.config.log_file_root_path": "Adresár logov", @@ -1122,7 +1111,6 @@ "admin.config.mailer_enable_helo": "Povolené HELO", "admin.config.mailer_name": "Meno", "admin.config.oauth_enabled": "Povolené", - "admin.config.disable_gravatar": "Zakázať Gravatar", "admin.config.enable_federated_avatar": "Povoliť avatary z verejných zdrojov", "admin.config.disabled_logger": "Zakázané", "admin.monitor.name": "Meno", @@ -1150,6 +1138,7 @@ "packages.conan.details.repository": "Repozitár", "packages.container.labels": "Štítky", "packages.container.labels.value": "Hodnota", + "packages.terraform.lock_status": "Stav zámku", "packages.owner.settings.cleanuprules.enabled": "Povolené", "secrets.creation.description": "Popis", "actions.actions": "Akcie", @@ -1160,5 +1149,7 @@ "actions.runners.task_list.repository": "Repozitár", "actions.runners.status.unspecified": "Neznámy", "actions.runners.version": "Verzia", - "git.filemode.symbolic_link": "Symbolický odkaz" + "actions.runs.branch": "Vetva", + "actions.workflow.scope_owner": "Vlastník", + "actions.workflow.scope_global": "Globálne" } diff --git a/options/locale/locale_sv-SE.json b/options/locale/locale_sv-SE.json index 6c9442a389..3da29e3dc7 100644 --- a/options/locale/locale_sv-SE.json +++ b/options/locale/locale_sv-SE.json @@ -120,12 +120,6 @@ "install.register_confirm": "Kräv Bekräftelse Via Mejl För Att Registrera", "install.mail_notify": "Aktivera Mejlnotifikationer", "install.server_service_title": "Inställningar för Server- och Tredjepartstjänster", - "install.offline_mode": "Aktivera Lokalt Läge", - "install.offline_mode_popup": "Inaktivera CDN från tredjepart och distribuera samtliga resurser lokalt istället.", - "install.disable_gravatar": "Inaktivera Gravatar", - "install.disable_gravatar_popup": "Inaktivera Gravatar- och avatarskällor från tredjepart. Om användaren inte laddar upp en avatar så kommer en standardavatar att användas.", - "install.federated_avatar_lookup": "Aktivera Federerade Avatarer", - "install.federated_avatar_lookup_popup": "Använd libravatar vid förenad uppslagning av avatarer.", "install.disable_registration": "Inaktivera Självregistrering", "install.disable_registration_popup": "Inaktivera självregistrering av användare. Endast administratörer kommer kunna skapa nya konton.", "install.allow_only_external_registration_popup": "Tillåt registrering endast via externa tjänster", @@ -144,10 +138,8 @@ "install.admin_email": "Mejladress", "install.install_btn_confirm": "Installera Gitea", "install.test_git_failed": "Misslyckades att testa 'git' kommando: %v", - "install.sqlite3_not_available": "Denna version av Gitea stödjer ej SQLite3. Ladda ner den officiella binären från %s (inte 'gobuild' versionen).", "install.invalid_db_setting": "Databasinställningarna är ogiltiga: %v", "install.invalid_repo_path": "Utvecklingskatalogens rotsökväg är ogiltig: %v", - "install.run_user_not_match": "Systemtjänstanvändaren är inte den nuvarande användaren: %s -> %s", "install.save_config_failed": "Misslyckades att spara konfigurationen: %v", "install.invalid_admin_setting": "Inställning för administartörskontot är ogiltig: %v", "install.invalid_log_root_path": "Sökvägen för loggar är ogiltig: %v", @@ -436,7 +428,6 @@ "settings.permission_read": "Lästa", "settings.manage_oauth2_applications": "Hantera OAuth2-applikationer", "settings.edit_oauth2_application": "Redigera OAuth2-applikation", - "settings.oauth2_applications_desc": "OAuth2-program tillåter ditt tredjepartsprogram att säkert autentisera användare på denna Gitea instans.", "settings.remove_oauth2_application": "Ta bort OAuth2-program", "settings.remove_oauth2_application_desc": "Borttagning av ett OAuth2-program kommer att upphäva åtkomsten för samtliga signerade åtkomst-tokens. Fortsätta?", "settings.remove_oauth2_application_success": "Applikationen har tagits bort.", @@ -449,7 +440,6 @@ "settings.oauth2_regenerate_secret": "Generera hemlighet på nytt", "settings.oauth2_regenerate_secret_hint": "Förlorat din hemlighet?", "settings.oauth2_application_edit": "Ändra", - "settings.oauth2_application_create_description": "OAuth2-applikationer ger tredjepartsapplikationer åtkomst till användarkonton på denna instans.", "settings.authorized_oauth2_applications": "Auktoriserade OAuth2-appar", "settings.revoke_key": "Upphäv", "settings.revoke_oauth2_grant": "Upphäv åtkomst", @@ -491,7 +481,6 @@ "repo.visibility": "Synligt för", "repo.visibility_description": "Bara ägaren eller medlemmar i organisationen med rätt rättigheter kommer kunna se det.", "repo.visibility_helper_forced": "Din tjänstadministratör påtvingar privata utvecklingskataloger.", - "repo.visibility_fork_helper": "(Ändring av detta kommer påverka alla forkar.)", "repo.clone_helper": "Hjälp med kloning? Se hjälp.", "repo.fork_repo": "Forka Repo", "repo.fork_from": "Forka Från", @@ -641,7 +630,6 @@ "repo.editor.commit_empty_file_text": "Filen du vill committa är tom. Vill du fortsätta?", "repo.editor.no_changes_to_show": "Det finns inga ändringar att visa.", "repo.editor.add_subdir": "Lägga till en katalog…", - "repo.editor.no_commit_to_branch": "Det gick inte att committa direkt till branchen för:", "repo.editor.user_no_push_to_branch": "Användaren kan inte pusha till branchen", "repo.editor.require_signed_commit": "Branchen kräver en signerad commit", "repo.commits.desc": "Bläddra i källkodens förändringshistorik.", @@ -681,11 +669,9 @@ "repo.issues.new": "Nytt Ärende", "repo.issues.new.title_empty": "Titeln kan inte vara tom", "repo.issues.new.labels": "Etiketter", - "repo.issues.new.no_label": "Ingen Etikett", "repo.issues.new.clear_labels": "Rensa etiketter", "repo.issues.new.projects": "Projekt", "repo.issues.new.clear_projects": "Rensa projekt", - "repo.issues.new.no_projects": "Inget projekt", "repo.issues.new.open_projects": "Öppna projekt", "repo.issues.new.closed_projects": "Stängda projekt", "repo.issues.new.no_items": "Inga objekt", @@ -767,7 +753,6 @@ "repo.issues.context.reference_issue": "Referens i nytt ärende", "repo.issues.context.edit": "Redigera", "repo.issues.context.delete": "Ta bort", - "repo.issues.reopen_issue": "Återöppna", "repo.issues.create_comment": "Kommentera", "repo.issues.closed_at": "stängde ärendet %[2]s", "repo.issues.reopened_at": "återöppnade detta ärende %[2]s", @@ -897,13 +882,11 @@ "repo.pulls.desc": "Aktivera pull-förfrågningar och kodgranskning.", "repo.pulls.new": "Ny Pull-Förfrågan", "repo.pulls.compare_changes": "Ny Pull-Request", - "repo.pulls.compare_changes_desc": "Välj branchen att merga in i, och ifrån.", "repo.pulls.compare_base": "merga in i", "repo.pulls.compare_compare": "pulla från", "repo.pulls.filter_branch": "Filtrera gren", - "repo.pulls.nothing_to_compare": "Dessa brancher är ekvivalenta. Det finns ingen anledning att skapa en pull-request.", "repo.pulls.create": "Skapa Pullförfrågan", - "repo.pulls.title_desc": "vill sammanfoga %[1]d incheckningar från s[2]s in i %[3]s", + "repo.pulls.title_desc": "vill sammanfoga %[1]d incheckningar från [2]s in i %[3]s", "repo.pulls.merged_title_desc": "sammanfogade %[1]d incheckningar från %[2]s in i %[3]s %[4]s", "repo.pulls.change_target_branch_at": "ändrade mål-branch från %s till %s%s", "repo.pulls.tab_conversation": "Konversation", @@ -1078,7 +1061,6 @@ "repo.settings.convert_fork_confirm": "Konvertera utvecklingskatalog", "repo.settings.transfer": "Överför Ägarskap", "repo.settings.transfer_desc": "Överför denna utvecklingskatalog till en användare eller organisation för vilken du har administratörsrättigheter till.", - "repo.settings.transfer_form_title": "Ange utvecklingskatalogens namn för att bekräfta:", "repo.settings.transfer_notices_1": "- Du kommer förlora åtkomst till denna utvecklingskatalog om du för över den till en individuell användare.", "repo.settings.transfer_notices_2": "- Du kommer behålla åtkomst till utvecklingskatalogen om du för över den till en organisation som du antingen äger eller är delägare i.", "repo.settings.transfer_owner": "Ny Ägare", @@ -1120,8 +1102,6 @@ "repo.settings.webhook.payload": "Innehåll", "repo.settings.webhook.body": "Innehåll", "repo.settings.githook_edit_desc": "Om kroken är inaktiv visas exempelinnehåll. Inaktivera denna krok genom att lämna innehållet tomt.", - "repo.settings.githook_name": "Kroknamn", - "repo.settings.githook_content": "Krokinnehåll", "repo.settings.update_githook": "Uppdatera krok", "repo.settings.add_webhook_desc": "Gitea kommer skicka ett POST anrop med en specificerad Content-Type till måladressen. Läs mer om detta i webhook guiden.", "repo.settings.payload_url": "Mål-URL", @@ -1191,7 +1171,6 @@ "repo.settings.add_protected_branch": "Aktivera skydd", "repo.settings.delete_protected_branch": "Inaktivera skydd", "repo.settings.protected_branch_deletion_desc": "Genom att inaktivera branchskyddet tillåts användare med skrivrättigheter att pusha till branchen. Vill du fortsätta?", - "repo.settings.default_branch_desc": "Välj en standard branch för Pull Requests och Code Commits:", "repo.settings.choose_branch": "Välj en branch…", "repo.settings.no_protected_branch": "Det finns inga skyddade brancher.", "repo.settings.edit_protected_branch": "Ändra", @@ -1220,6 +1199,7 @@ "repo.settings.lfs_force_unlock": "Tvinga upplåsning", "repo.settings.lfs_pointers.inRepo": "I utvecklingskatalogen", "repo.settings.rename_branch_failed_not_exist": "Kan inte byta namn på branchen %s eftersom den inte finns.", + "repo.settings.rename_branch": "Döp om gren", "repo.diff.browse_source": "Bläddra i källkod", "repo.diff.parent": "förälder", "repo.diff.commit": "incheckning", @@ -1252,6 +1232,7 @@ "repo.diff.review.approve": "Godkänn", "repo.diff.review.reject": "Begär ändringar", "repo.diff.committed_by": "committad av", + "repo.commits.avatar_stack_and": "och", "repo.releases.desc": "Följ projektversioner och nerladdningar.", "repo.release.releases": "Släpp", "repo.release.tags": "Taggar", @@ -1276,6 +1257,7 @@ "repo.release.save_draft": "Spara Utkast", "repo.release.edit_release": "Uppdatera Release", "repo.release.delete_release": "Ta bort Release", + "repo.release.delete_tag": "Ta bort tagg", "repo.release.deletion": "Ta bort Release", "repo.release.deletion_success": "Releasen har blivit raderad.", "repo.release.tag_name_already_exist": "En release med denna tagg existerar redan.", @@ -1284,9 +1266,16 @@ "repo.release.download_count": "Nedladdningar: %s", "repo.branch.name": "Branch namn", "repo.branch.delete_head": "Radera", + "repo.branch.delete": "Ta bort gren \"%s\"", "repo.branch.delete_html": "Radera branch", "repo.branch.create_branch": "Skapa branchen %s", + "repo.branch.create_from": "från \"%s\"", "repo.branch.deleted_by": "Raderad av %s", + "repo.branch.confirm_create_branch": "Skapa gren", + "repo.branch.confirm_rename_branch": "Döp om gren", + "repo.branch.create_branch_operation": "Skapa gren", + "repo.tag.create_tag_operation": "Skapa tagg", + "repo.tag.confirm_create_tag": "Skapa tagg", "repo.topic.manage_topics": "Hantera ämnen", "repo.topic.done": "Klar", "org.org_name_holder": "Organisationsnamn", @@ -1332,9 +1321,7 @@ "org.settings.labels_desc": "Lägg till etiketter som kan användas till ärenden för alla utvecklingskataloger under denna organisation.", "org.members.membership_visibility": "Synlighet för medlemskap:", "org.members.public": "Synlig", - "org.members.public_helper": "gör dold", "org.members.private": "Dold", - "org.members.private_helper": "gör synlig", "org.members.member_role": "Medlemsroll:", "org.members.owner": "Ägare", "org.members.member": "Medlem", @@ -1358,7 +1345,6 @@ "org.teams.members": "Teammedlemmar", "org.teams.update_settings": "Uppdatera inställningar", "org.teams.delete_team": "Ta bort team", - "org.teams.add_team_member": "Lägg till teammedlem", "org.teams.delete_team_title": "Ta bort team", "org.teams.delete_team_desc": "Borttagning av ett team återkallar åtkomsten till utvecklingskatalogen för dess medlemmar. Vill du fortsätta?", "org.teams.delete_team_success": "Teamet har blivit borttaget.", @@ -1381,12 +1367,18 @@ "org.teams.all_repositories_read_permission_desc": "Detta team beviljar Läs-rättigheter till alla utvecklingskataloger: medlemmar kan visa och klona utvecklingskataloger.", "org.teams.all_repositories_write_permission_desc": "Detta team beviljar Skriv-rättigheter till alla utvecklingskataloger: medlemmar kan läsa från och pusha till utvecklingskataloger.", "org.teams.all_repositories_admin_permission_desc": "Detta team beviljar Admin-rättigheter till alla utvecklingskataloger: medlemmar kan läsa från, pusha till och lägga till kollaboratörer för utvecklingskatalogerna.", + "org.teams.visibility": "Synligt för", + "org.teams.visibility_private": "Privat", + "org.teams.visibility_public": "Offentlig", + "org.worktime.date_range_start": "Start-datum", + "org.worktime.date_range_end": "Slutdatum", "admin.dashboard": "Instrumentpanel", "admin.users": "Användarkonto", "admin.organizations": "Organisationer", "admin.repositories": "Utvecklingskataloger", "admin.hooks": "Webbhookar", "admin.authentication": "Autentiseringskälla", + "admin.badges.description": "Beskrivning", "admin.config": "Konfiguration", "admin.config_summary": "Översikt", "admin.config_settings": "Inställningar", @@ -1547,9 +1539,7 @@ "admin.auths.deletion_success": "Autentiseringskällan har tagits bort.", "admin.config.server_config": "Server-konfiguration", "admin.config.app_name": "Sajtens namn", - "admin.config.app_url": "Gitea Bas-URL", "admin.config.custom_conf": "Konfigurationsfil", - "admin.config.offline_mode": "Offlineläge", "admin.config.disable_router_log": "Avaktivera Router Loggning", "admin.config.run_user": "Kör som användarnamn", "admin.config.run_mode": "Exekveringsläge", @@ -1607,10 +1597,8 @@ "admin.config.cache_config": "Mellanlagringskonfiguration", "admin.config.cache_adapter": "Mellanlagringsadapter", "admin.config.cache_interval": "Mellanlagringsintervall", - "admin.config.cache_conn": "Mellanlagringsanslutning", "admin.config.session_config": "Sessionskonfiguration", "admin.config.session_provider": "Sessionsleverantör", - "admin.config.provider_config": "Leverantörskonfiguration", "admin.config.cookie_name": "Cookie-namn", "admin.config.gc_interval_time": "Tidsintervall för skräpsamling", "admin.config.session_life_time": "Livstid för session", @@ -1618,7 +1606,6 @@ "admin.config.cookie_life_time": "Livstid för kaka", "admin.config.picture_config": "Konfiguration för bild och avatar", "admin.config.picture_service": "Bildtjänst", - "admin.config.disable_gravatar": "Inaktivera Gravatar", "admin.config.enable_federated_avatar": "Aktivera Förenad Uppslaging av Profilbilder", "admin.config.git_config": "Git-konfiguration", "admin.config.git_disable_diff_highlight": "Inaktivera Diff Syntax Highlight", @@ -1628,8 +1615,6 @@ "admin.config.git_gc_args": "Skräpsamlarargument", "admin.config.git_migrate_timeout": "Migreringstimeout", "admin.config.git_mirror_timeout": "Spelgingsuppdateringstimeout", - "admin.config.git_clone_timeout": "Klonoperationstimeout", - "admin.config.git_pull_timeout": "Klonoperationstimeout", "admin.config.git_gc_timeout": "GC-operationstimeout", "admin.config.log_config": "Logg-konfiguration", "admin.config.disabled_logger": "Inaktiverad", @@ -1724,6 +1709,8 @@ "packages.conan.details.repository": "Utvecklingskatalog", "packages.container.labels": "Etiketter", "packages.npm.dependencies": "Beroenden", + "packages.terraform.lock": "Lås", + "packages.terraform.unlock": "Lås upp", "packages.owner.settings.cleanuprules.enabled": "Aktiv", "secrets.creation.description": "Beskrivning", "actions.runners.name": "Namn", @@ -1733,5 +1720,6 @@ "actions.runners.task_list.run": "Kör", "actions.runners.task_list.repository": "Utvecklingskatalog", "actions.runners.status.active": "Aktiv", - "git.filemode.symbolic_link": "Symbolisk länk" + "actions.runs.summary": "Översikt", + "actions.workflow.scope_owner": "Ägare" } diff --git a/options/locale/locale_tr-TR.json b/options/locale/locale_tr-TR.json index edd2db2e39..585fa5aebe 100644 --- a/options/locale/locale_tr-TR.json +++ b/options/locale/locale_tr-TR.json @@ -224,7 +224,6 @@ "startpage.lightweight": "Hafif", "startpage.lightweight_desc": "Gitea'nın minimal gereksinimleri çok düşüktür ve ucuz bir Raspberry Pi üzerinde çalışabilmektedir. Makine enerjinizden tasarruf edin!", "startpage.license": "Açık Kaynak", - "startpage.license_desc": "Gidin ve code.gitea.io/gitea'yı edinin! Bu projeyi daha da iyi yapmak için katkıda bulunarak bize katılın. Katkıda bulunmaktan çekinmeyin!", "install.install": "Kurulum", "install.installing_desc": "Şimdi kuruluyor, lütfen bekleyin…", "install.title": "Başlangıç Yapılandırması", @@ -261,7 +260,6 @@ "install.lfs_path": "Git LFS Kök Yolu", "install.lfs_path_helper": "Git LFS tarafından izlenen dosyalar bu dizinde saklanacaktır. LFS'yi devre dışı bırakmak için boş bırakın.", "install.run_user": "Şu Kullanıcı Olarak Çalıştır", - "install.run_user_helper": "Gitea'nin çalışacağı işletim sistemi kullanıcı adı. Bu kullanıcının depo kök yoluna erişiminin olması gerektiğini unutmayın.", "install.domain": "Sunucu Alan Adı", "install.domain_helper": "Sunucu için alan adı veya ana bilgisayar adresi.", "install.ssh_port": "SSH Sunucu Portu", @@ -284,12 +282,6 @@ "install.register_confirm": "Kayıt için E-posta Doğrulaması Gereksin", "install.mail_notify": "E-Posta Bildirimlerini Etkinleştir", "install.server_service_title": "Sunucu ve Diğer Servis Ayarları", - "install.offline_mode": "Yerel Kipi Etkinleştir", - "install.offline_mode_popup": "Üçüncü parti içerik teslim ağlarını etkisizleştirin ve bütün kaynakları yerelden sunun.", - "install.disable_gravatar": "Gravatar'ı Devre Dışı Bırak", - "install.disable_gravatar_popup": "Gravatar ve üçüncü parti avatar kaynaklarını iptal edin. Kullanıcı bir avatar yüklemediği zaman varsayılan bir avatar kullanılacaktır.", - "install.federated_avatar_lookup": "Birleştirilmiş Avatarları Etkinleştir", - "install.federated_avatar_lookup_popup": "Libravatar kullanarak federe avatar aramasını etkinleştirin.", "install.disable_registration": "Kendi Kendine Kaydolmayı Devre Dışı Bırak", "install.disable_registration_popup": "Kullanıcının kendi kendine kaydolmasını devre dışı bırak. Yalnızca yöneticiler yeni hesaplar oluşturabilecek.", "install.allow_only_external_registration_popup": "Sadece dış hizmetler aracılığıyla kullanıcı kaydına izin ver", @@ -309,12 +301,10 @@ "install.admin_email": "E-posta Adresi", "install.install_btn_confirm": "Gitea'u Kur", "install.test_git_failed": "'git' komut testi başarısız: %v", - "install.sqlite3_not_available": "Bu Gieta sürümü SQLite3 desteklemiyor. Lütfen %s adresinden resmi çalışır sürümü ('gobuild' sürümünü değil) indirin.", "install.invalid_db_setting": "Veritabanı ayarları geçersiz: %v", "install.invalid_db_table": "\"%s\" veritabanı tablosu geçersiz: %v", "install.invalid_repo_path": "Depo kök dizini geçersiz: %v", "install.invalid_app_data_path": "Uygulama veri yolu geçersiz: %v", - "install.run_user_not_match": "'Birlikte çalıştır' kullanıcı adı şimdiki kullanıcı adından farklıdır: %s -> %s", "install.internal_token_failed": "Dahili belirteç oluşturulamadı: %v", "install.secret_key_failed": "Gizli anahtar oluşturulamadı: %v", "install.save_config_failed": "%v Yapılandırması kaydedilirken hata oluştu", @@ -635,14 +625,6 @@ "user.block.unblock.failure": "Kullanıcının engeli kaldırılamadı: %s", "user.block.blocked": "Bu kullanıcıyı engelledin.", "user.block.title": "Bir kullanıcı engelle", - "user.block.info": "Bir kullanıcıyı engellemek depoarla, değişiklik isteği veya konu açmak veya yorumlamak gibi, etkileşim kurmasını önler. Bir kullanıcı engelleme hakkında daha fazlasını öğrenin.", - "user.block.info_1": "Bir kullanıcıyı engellemek, hesabınızda ve depolarınızda şu eylemleri önler:", - "user.block.info_2": "hesabınızı takip etmek", - "user.block.info_3": "kullanıcı adınızdan @bahsederek size bildirim göndermek", - "user.block.info_4": "kendi depolarına sizi katkıcı olarak davet etmek", - "user.block.info_5": "depolara yıldız koymak, çatallamak veya izlemek", - "user.block.info_6": "konu veya değişiklik isteği açmak ve yorum eklemek", - "user.block.info_7": "konularda veya değişiklik isteklerinde yorumlarınıza tepki vermek", "user.block.user_to_block": "Engellenecek kullanıcı", "user.block.note": "Not", "user.block.note.title": "İsteğe bağlı not:", @@ -650,6 +632,7 @@ "user.block.note.edit": "Notu düzenle", "user.block.list": "Engellenmiş kullanıcılar", "user.block.list.none": "Engellediğiniz kullanıcı yok.", + "settings.general": "Genel", "settings.profile": "Profil", "settings.account": "Hesap", "settings.appearance": "Görünüm", @@ -871,7 +854,6 @@ "settings.permissions_list": "İzinler:", "settings.manage_oauth2_applications": "OAuth2 Uygulamalarını Yönet", "settings.edit_oauth2_application": "OAuth2 Uygulamalarını Düzenle", - "settings.oauth2_applications_desc": "OAuth2 uygulamaları, üçüncü parti uygulamanızın bu Gitea örneğinde kullanıcıları güvenli bir şekilde doğrulamasını sağlar.", "settings.remove_oauth2_application": "OAuth2 Uygulamasını Kaldır", "settings.remove_oauth2_application_desc": "Bir OAuth2 uygulamasının kaldırılması, imzalı tüm erişim jetonlarına erişimi iptal eder. Devam edilsin mi?", "settings.remove_oauth2_application_success": "Uygulama silindi.", @@ -890,7 +872,6 @@ "settings.oauth2_regenerate_secret_hint": "Gizliliğini mi kaybettin?", "settings.oauth2_client_secret_hint": "Bu sayfadan ayrıldıktan veya yeniledikten sonra gizliliği göremeyeceksiniz. Kaydettiğinizden emin olun.", "settings.oauth2_application_edit": "Düzenle", - "settings.oauth2_application_create_description": "OAuth2 uygulamaları, üçüncü taraf uygulamanıza bu durumda kullanıcı hesaplarına erişim sağlar.", "settings.oauth2_application_remove_description": "Bir OAuth2 uygulamasının kaldırılması, bu sunucudaki yetkili kullanıcı hesaplarına erişmesini önler. Devam edilsin mi?", "settings.oauth2_application_locked": "Gitea kimi OAuth2 uygulamalarının başlangıçta ön kaydını, yapılandırmada etkinleştirilmişse yapabilir. Beklenmeyen davranışı önlemek için bunlar ne düzenlenmeli ne de kaldırılmalı. Daha fazla bilgi için OAuth2 belgesine bakın.", "settings.authorized_oauth2_applications": "Yetkili OAuth2 Uygulamaları", @@ -970,7 +951,6 @@ "repo.visibility_description": "Yalnızca sahibi veya haklara sahip organizasyon üyeleri onu görebilecek.", "repo.visibility_helper": "Depoyu özel yap", "repo.visibility_helper_forced": "Site yöneticiniz, yeni depoları gizli olmaya zorluyor.", - "repo.visibility_fork_helper": "(Bunu değiştirmek tüm çatallamaları etkileyecektir.)", "repo.clone_helper": "Klonlama konusunda yardıma mı ihtiyacınız var? Yardım adresini ziyaret edin.", "repo.fork_repo": "Depoyu Çatalla", "repo.fork_from": "Buradan Çatalla", @@ -1062,8 +1042,6 @@ "repo.transfer.accept_desc": "\"%s\" tarafına aktar", "repo.transfer.reject": "Aktarımı Reddet", "repo.transfer.reject_desc": "\"%s\" tarafına aktarımı iptal et", - "repo.transfer.no_permission_to_accept": "Bu aktarımı kabul etme izniniz yok.", - "repo.transfer.no_permission_to_reject": "Bu aktarımı reddetme izniniz yok.", "repo.desc.private": "Özel", "repo.desc.public": "Genel", "repo.desc.public_access": "Herkese Açık Erişim", @@ -1214,7 +1192,6 @@ "repo.ambiguous_runes_description": "Bu dosya, başka karakterlerle karıştırılabilecek evrensel kodlu karakter içeriyor. Eğer bunu kasıtlı olarak yaptıysanız bu uyarıyı yok sayabilirsiniz. Gizli karakterleri göstermek için Kaçış Karakterli düğmesine tıklayın.", "repo.invisible_runes_line": "Bu satırda görünmez evrensel kodlu karakter var", "repo.ambiguous_runes_line": "Bu satırda muğlak evrensel kodlu karakter var", - "repo.ambiguous_character": "%[1]c [U+%04[1]X], %[2]c [U+%04[2]X] ile karıştırılabilir", "repo.escape_control_characters": "Kaçış Karakterli", "repo.unescape_control_characters": "Kaçış Karaktersiz", "repo.file_copy_permalink": "Kalıcı Bağlantıyı Kopyala", @@ -1307,7 +1284,6 @@ "repo.editor.upload_file_is_locked": "\"%s\" dosyası %s tarafından kilitlendi.", "repo.editor.upload_files_to_dir": "Dosyaları \"%s\" dizinine yükle", "repo.editor.cannot_commit_to_protected_branch": "Korunan \"%s\" dalına işleme yapılamıyor.", - "repo.editor.no_commit_to_branch": "Doğrudan dala işleme yapılamıyor çünkü:", "repo.editor.user_no_push_to_branch": "Kullanıcı dala gönderemez", "repo.editor.require_signed_commit": "Dal imzalı bir işleme gerektirir", "repo.editor.cherry_pick": "%s şunun üzerine cımbızla:", @@ -1323,7 +1299,6 @@ "repo.commits.desc": "Kaynak kodu değişiklik geçmişine göz atın.", "repo.commits.commits": "İşleme", "repo.commits.no_commits": "Ortak bir işleme yok. \"%s\" ve \"%s\" tamamen farklı geçmişlere sahip.", - "repo.commits.nothing_to_compare": "Bu dallar eşit.", "repo.commits.search.tooltip": "Anahtar kelimeleri \"author:\", \"committer:\", \"after:\" veya \"before:\" ile kullanabilirsiniz, örneğin \"revert author:Alice before:2019-01-13\".", "repo.commits.search_branch": "Bu Dal", "repo.commits.search_all": "Tüm Dallar", @@ -1400,11 +1375,9 @@ "repo.issues.new": "Yeni Konu", "repo.issues.new.title_empty": "Başlık boş olamaz", "repo.issues.new.labels": "Etiketler", - "repo.issues.new.no_label": "Etiket Yok", "repo.issues.new.clear_labels": "Etiketleri temizle", "repo.issues.new.projects": "Projeler", "repo.issues.new.clear_projects": "Projeleri temizle", - "repo.issues.new.no_projects": "Proje yok", "repo.issues.new.open_projects": "Projeleri Aç", "repo.issues.new.closed_projects": "Projeleri Kapat", "repo.issues.new.no_items": "Öge yok", @@ -1533,7 +1506,6 @@ "repo.issues.comment_pull_merged_at": "%[1]s işlemesi, %[2]s dalına birleştirildi %[3]s", "repo.issues.comment_manually_pull_merged_at": "%[1]s işlemesi, %[2]s dalına elle birleştirildi %[3]s", "repo.issues.close_comment_issue": "Yorum Yap ve Kapat", - "repo.issues.reopen_issue": "Yeniden aç", "repo.issues.reopen_comment_issue": "Yorum Yap ve Yeniden Aç", "repo.issues.create_comment": "Yorum yap", "repo.issues.comment.blocked_user": "Yorum oluşturulamıyor veya düzenlenemiyor, gönderen veya depo sahibi tarafından engellenmişsiniz.", @@ -1778,8 +1750,6 @@ "repo.pulls.select_commit_hold_shift_for_range": "İşleme seç. Bir aralık seçmek için Shift'e basılı tutup tıklayın.", "repo.pulls.review_only_possible_for_full_diff": "İnceleme sadece tam fark görüntülemede mümkündür", "repo.pulls.filter_changes_by_commit": "İşleme ile süz", - "repo.pulls.nothing_to_compare": "Bu dallar eşit. Değişiklik isteği oluşturmaya gerek yok.", - "repo.pulls.nothing_to_compare_have_tag": "Seçili dal/etiket aynı.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "Bu dallar eşittir. Bu Dİ boş olacak.", "repo.pulls.has_pull_request": "Bu dallar arasında zaten bir değişiklik isteği var: %[2]s#%[3]d", "repo.pulls.create": "Değişiklik İsteği Oluştur", @@ -1949,7 +1919,6 @@ "repo.signing.wont_sign.headsigned": "Ana işleme imzalanmadığı için birleştirme imzalanmayacak.", "repo.signing.wont_sign.commitssigned": "İlişkili tüm işlemeler imzalanmadığı için birleştirme imzalanmayacak.", "repo.signing.wont_sign.approved": "Değişiklik İsteği onaylanmadığı için birleştirme imzalanmayacak.", - "repo.signing.wont_sign.not_signed_in": "Oturum açmadınız.", "repo.ext_wiki": "Harici Vikiye Erişim", "repo.ext_wiki.desc": "Harici bir wiki'ye bağlantı.", "repo.wiki": "Viki", @@ -2173,7 +2142,6 @@ "repo.settings.transfer_abort_invalid": "Var olmayan bir depo aktarımını iptal edemezsiniz.", "repo.settings.transfer_abort_success": "%s tarafına yapılan depo aktarımı başarıyla iptal edildi.", "repo.settings.transfer_desc": "Bu depoyu bir kullanıcıya veya yönetici haklarına sahip olduğunuz bir organizasyona aktarın.", - "repo.settings.transfer_form_title": "Onaylamak için depo adını girin:", "repo.settings.transfer_in_progress": "Şu anda devam etmekte olan bir aktarım mevcut. Eğer bu depoyu başka bir kullanıcıya aktarmak istiyorsanız mevcut aktarımı iptal edin.", "repo.settings.transfer_notices_1": "- Bireysel bir kullanıcıya aktarırsanız depoya erişiminizi kaybedersiniz.", "repo.settings.transfer_notices_2": "- Sahip (-yardımcı) olduğunuz bir organizasyona devrederseniz, depoya erişmeye devam edersiniz.", @@ -2192,10 +2160,8 @@ "repo.settings.trust_model.collaborator.desc": "Bu deponun katkıcılarının geçerli imzaları, işleyici ile eşleşse de eşleşmese de, \"güvenilir\" olarak işaretlenecektir. Aksi takdirde, imzanın işleyiciyle eşleşmesi durumunda geçerli imzalar \"güvenilmez\", eşleşmiyorsa \"eşleşmemiş\" olarak işaretlenir.", "repo.settings.trust_model.committer": "İşleyici", "repo.settings.trust_model.committer.long": "İşleyici: İşleyicilerle eşleşen imzalara güvenin. Bu GitHub'ın davranışına uygundur ve Gitea imzalı işlemeleri işleyen olarak Gitea'ya sahip olmaya zorlar.", - "repo.settings.trust_model.committer.desc": "Geçerli imzalar yalnızca işleyiciyle eşleşiyorsa \"güvenilir\" olarak işaretlenir, aksi takdirde \"eşleşmemiş\" olarak işaretlenir. Bu, Gitea'yı işlemede Ortak Yazan: ve Ortak İşlenen: fragmanı olarak işaretlenen gerçek kaydediciyle imzalanan işlemelerde işleyici olmayı zorlayacaktır. Varsayılan Gitea anahtarı, veritabanındaki bir Kullanıcıyla eşleşmelidir.", "repo.settings.trust_model.collaboratorcommitter": "Katkıcı+İşleyici", "repo.settings.trust_model.collaboratorcommitter.long": "Katkıcı+İşleyen: İşleyenle eşleşen katkıcıların imzalarına güvenin", - "repo.settings.trust_model.collaboratorcommitter.desc": "Bu deponun katkıcılarının geçerli imzaları, işleyici ile eşleşiyorlarsa \"güvenilir\" olarak işaretlenecektir. Aksi takdirde, imza işleyiciyle eşleşiyorsa geçerli imzalar \"güvenilmez\", aksi takdirde \"eşleşmiyor\" olarak işaretlenir. Bu, Gitea'yı işlemede Ortak Yazan: ve Ortak İşlenen: fragmanı olarak işaretlenmiş gerçek işleyici ile imzalı işlemelerde işleyici olarak işaretlenmeye zorlayacaktır. Varsayılan Gitea anahtarı, veritabanındaki bir Kullanıcıyla eşleşmelidir.", "repo.settings.wiki_delete": "Wiki Verisini Sil", "repo.settings.wiki_delete_desc": "Depo wiki verilerini silmek kalıcıdır ve geri alınamaz.", "repo.settings.wiki_delete_notices_1": "- Bu işlem, %s için depo wiki'sini kalıcı olarak siler ve devre dışı bırakır.", @@ -2238,7 +2204,6 @@ "repo.settings.webhook_deletion_success": "Web isteği silindi.", "repo.settings.webhook.test_delivery": "İtme Olayını Sına", "repo.settings.webhook.test_delivery_desc": "Bu web istemcisini sahte bir itme olayıyla sınayın.", - "repo.settings.webhook.test_delivery_desc_disabled": "Bu web istemcisini sahte bir olayla denemek için etkinleştirin.", "repo.settings.webhook.request": "İstekler", "repo.settings.webhook.response": "Cevaplar", "repo.settings.webhook.headers": "Başlıklar", @@ -2249,8 +2214,6 @@ "repo.settings.webhook.delivery.success": "Teslim kuyruğuna bir olay eklendi. Teslim geçmişinde görünmesi birkaç saniye alabilir.", "repo.settings.githooks_desc": "Git İstemcileri Git'in kendisi tarafından desteklenmektedir. Özel işlemler ayarlamak için aşağıdaki istemci dosyalarını düzenleyebilirsiniz.", "repo.settings.githook_edit_desc": "İstek aktif değilse örnek içerik sunulacaktır. İçeriği boş bırakmak, isteği devre dışı bırakmayı beraberinde getirecektir.", - "repo.settings.githook_name": "İstek İsmi", - "repo.settings.githook_content": "İstek İçeriği", "repo.settings.update_githook": "İsteği Güncelle", "repo.settings.add_webhook_desc": "Gitea, belirtilen içerik türüne sahip POST isteğini hedef URL’ye gönderecektir. Web istemcileri kılavuzundan daha fazla bilgi edinebilirsiniz.", "repo.settings.payload_url": "Hedef URL", @@ -2445,7 +2408,6 @@ "repo.settings.block_outdated_branch": "Değişiklik isteği güncel değilse birleştirmeyi engelle", "repo.settings.block_outdated_branch_desc": "Baş dal taban dalın arkasındayken birleştirme mümkün olmayacaktır.", "repo.settings.block_admin_merge_override": "Yöneticiler dal koruma kurallarına uymalıdır", - "repo.settings.block_admin_merge_override_desc": "Yöneticiler dal koruma kurallarına uymalıdır ve kurallardan kaçınamazlar.", "repo.settings.default_branch_desc": "Kod işlemeleri için varsayılan bir depo dalı seçin.", "repo.settings.default_target_branch_desc": "Değişiklik istekleri, Depo Gelişmiş Ayarları'nın Değişiklik İstekleri bölümünde ayarlanmışsa farklı varsayılan hedef dal kullanabilir.", "repo.settings.merge_style_desc": "Biçimleri Birleştir", @@ -2476,7 +2438,6 @@ "repo.settings.visibility.private.text": "Görünürlüğü özele çevirmek depoyu sadece izin verilen üyeler görecek ancak kendisiyle çatalları, izleyenler ve yıldızlar arasındaki ilişkiyi ortadan kaldırabilir.", "repo.settings.visibility.private.bullet_title": "Görünürlüğü özele çevirmek:", "repo.settings.visibility.private.bullet_one": "Depoyu sadece izin verilen kullanıcılar görecek.", - "repo.settings.visibility.private.bullet_two": "Kendisiyle çatalları, izleyenler, ve yıldızlar arasındaki ilişkiyi ortadan kaldırabilir.", "repo.settings.visibility.public.button": "Açık Yap", "repo.settings.visibility.public.text": "Görünürlüğü açık yaptığınızda depo herkes tarafından görünecektir.", "repo.settings.visibility.public.bullet_title": "Görünürlüğü açığa çevirmek:", @@ -2577,6 +2538,7 @@ "repo.diff.review.reject": "Değişiklik iste", "repo.diff.review.self_approve": "Değişiklik isteği sahipleri kendi değişiklik isteklerini onaylayamazlar", "repo.diff.committed_by": "işlemeyi yapan:", + "repo.commits.avatar_stack_and": "ve", "repo.diff.protected": "Korumalı", "repo.diff.image.side_by_side": "Yan Yana", "repo.diff.image.swipe": "Kaydır", @@ -2774,9 +2736,7 @@ "org.settings.labels_desc": "Bu organizasyonun altındaki tüm depolar ile ilgili konularda kullanılabilecek etiketler ekleyin.", "org.members.membership_visibility": "Üyelik Görünürlüğü:", "org.members.public": "Görünür", - "org.members.public_helper": "gizle", "org.members.private": "Gizlenmiş", - "org.members.private_helper": "görünür yap", "org.members.member_role": "Üye Rolü:", "org.members.owner": "Sahibi", "org.members.member": "Üye", @@ -2807,7 +2767,6 @@ "org.teams.members": "Takım Üyeleri", "org.teams.update_settings": "Ayarları Güncelle", "org.teams.delete_team": "Takımı Sil", - "org.teams.add_team_member": "Takım Üyesi Ekle", "org.teams.invite_team_member": "%s daveti", "org.teams.invite_team_member.list": "Bekleyen Davetler", "org.teams.delete_team_title": "Takımı Sil", @@ -2834,6 +2793,10 @@ "org.teams.all_repositories_read_permission_desc": "Bu takım tüm depolara Okuma erişimi sağlar: üyeler depoları görüntüleyebilir ve kopyalayabilir.", "org.teams.all_repositories_write_permission_desc": "Bu takım tüm depolara Yazma erişimi sağlar: üyeler depolardan okuyabilir ve depolara itebilir.", "org.teams.all_repositories_admin_permission_desc": "Bu takım tüm depolara Yönetici erişimi sağlar: üyeler depolardan okuyabilir, itebilir ve katkıcıları ekleyebilir.", + "org.teams.visibility": "Görünürlük", + "org.teams.visibility_private": "Özel", + "org.teams.visibility_limited": "Sınırlı", + "org.teams.visibility_public": "Genel", "org.teams.invite.title": "%s takımına (Organizasyon: %s) katılmaya davet edildiniz.", "org.teams.invite.by": "%s tarafından davet edildi", "org.teams.invite.description": "Takıma katılmak için aşağıdaki düğmeye tıklayın.", @@ -2859,6 +2822,7 @@ "admin.hooks": "Web İstemcileri", "admin.integrations": "Bütünleştirmeler", "admin.authentication": "Yetkilendirme Kaynakları", + "admin.badges.description": "Açıklama", "admin.emails": "Kullanıcı E-postaları", "admin.config": "Yapılandırma", "admin.config_summary": "Özet", @@ -3187,11 +3151,8 @@ "admin.config.server_config": "Sunucu Yapılandırması", "admin.config.app_name": "Site Başlığı", "admin.config.app_ver": "Gitea Sürümü", - "admin.config.app_url": "Gitea Taban URL'si", "admin.config.custom_conf": "Yapılandırma Dosyası Yolu", "admin.config.custom_file_root_path": "Özel Dosya Kök Yolu", - "admin.config.domain": "Sunucu Alan Adı", - "admin.config.offline_mode": "Yerel Kip", "admin.config.disable_router_log": "Yönlendirici Log'larını Devre Dışı Bırak", "admin.config.run_user": "Şu Kullanıcı Olarak Çalıştır", "admin.config.run_mode": "Çalıştırma Modu", @@ -3242,7 +3203,6 @@ "admin.config.default_enable_timetracking": "Varsayılan Olarak Zaman İzlemeyi Etkinleştir", "admin.config.default_allow_only_contributors_to_track_time": "Yalnızca Katkıda Bulunanlar Zaman İzlesin", "admin.config.no_reply_address": "Gizlenmiş E-Postalar için Alan Adı", - "admin.config.default_visibility_organization": "Yeni organizasyonlar için varsayılan görünürlük", "admin.config.default_enable_dependencies": "Konu Bağımlılıklarını Varsayılan Olarak Etkinleştir", "admin.config.webhook_config": "Web İstemcileri Yapılandırması", "admin.config.queue_length": "Kuyruk Uzunluğu", @@ -3271,7 +3231,6 @@ "admin.config.cache_config": "Önbellek Yapılandırması", "admin.config.cache_adapter": "Önbellek Uyarlayıcısı", "admin.config.cache_interval": "Önbellek Aralığı", - "admin.config.cache_conn": "Önbellek Bağlantısı", "admin.config.cache_item_ttl": "TTL Önbellek Öğesi", "admin.config.cache_test": "Önbelleği Sına", "admin.config.cache_test_failed": "Önbelleğin incelenmesi başarısız oldu: %v.", @@ -3279,7 +3238,6 @@ "admin.config.cache_test_succeeded": "Önbellek sınaması başarılı, %s sürede bir yanıt alındı.", "admin.config.session_config": "Oturum Yapılandırması", "admin.config.session_provider": "Oturum Sağlayıcı", - "admin.config.provider_config": "Sağlayıcı Yapılandırması", "admin.config.cookie_name": "Çerez Adı", "admin.config.gc_interval_time": "GC Aralık Zamanı", "admin.config.session_life_time": "Oturum Yaşam Zamanı", @@ -3287,7 +3245,6 @@ "admin.config.cookie_life_time": "Çerez Yaşam Zamanı", "admin.config.picture_config": "Resim ve Avatar Yapılandırması", "admin.config.picture_service": "Resim Servisi", - "admin.config.disable_gravatar": "Gravatar Hizmet Dışı", "admin.config.enable_federated_avatar": "Birleştirilmiş Avatarları Etkinleştir", "admin.config.open_with_editor_app_help": "Klon menüsü için \"Birlikte aç\" düzenleyicileri. Boş bırakılırsa, varsayılan kullanılacaktır. Varsayılanı görmek için genişletin.", "admin.config.git_guide_remote_name": "Kılavuzdaki git komutları için deponun uzak adı", @@ -3560,9 +3517,12 @@ "packages.swift.registry": "Bu kütüğü komut satırını kullanarak kurun:", "packages.swift.install": "Paketi Package.swift dosyanıza ekleyin:", "packages.swift.install2": "ve şu komutu çalıştırın:", + "packages.terraform.install2": "ve şu komutu çalıştırın:", + "packages.terraform.lock_status": "Kilit Durumu", + "packages.terraform.lock": "Kilitle", + "packages.terraform.unlock": "Kilidi Aç", "packages.vagrant.install": "Vagrant paketi eklemek için aşağıdaki komutu çalıştırın:", "packages.settings.link": "Bu paketi bir depoya bağlayın", - "packages.settings.link.description": "Eğer bir paketi bir depoya bağlarsanız, paket deponun paket listesinde listelenecektir.", "packages.settings.link.select": "Depo Seç", "packages.settings.link.button": "Depo Bağlantısını Güncelle", "packages.settings.link.success": "Depo bağlantısı başarıyla güncellendi.", @@ -3682,6 +3642,8 @@ "actions.runs.status": "Durum", "actions.runs.actors_no_select": "Tüm aktörler", "actions.runs.status_no_select": "Tüm durumlar", + "actions.runs.branch": "Dal", + "actions.runs.branches_no_select": "Tüm dallar", "actions.runs.no_results": "Eşleşen sonuç yok.", "actions.runs.no_workflows": "Henüz hiç bir iş akışı yok.", "actions.runs.no_workflows.quick_start": "Gitea İşlemlerini nasıl başlatacağınızı bilmiyor musunuz? Hızlı başlangıç kılavuzuna bakabilirsiniz.", @@ -3694,11 +3656,16 @@ "actions.runs.delete.description": "Bu iş akışı çalışmasını kalıcı olarak silmek istediğinizden emin misiniz? Bu eylem geri alınamaz.", "actions.runs.not_done": "Bu iş akışı tamamlanmadı.", "actions.runs.view_workflow_file": "İş akışı dosyasını görüntüle", + "actions.runs.summary": "Özet", + "actions.runs.latest": "En son", "actions.workflow.disable": "İş Akışını Devre Dışı Bırak", "actions.workflow.disable_success": "'%s' iş akışı başarıyla devre dışı bırakıldı.", "actions.workflow.enable": "İş Akışını Etkinleştir", "actions.workflow.enable_success": "'%s' iş akışı başarıyla etkinleştirildi.", "actions.workflow.disabled": "İş akışı devre dışı.", + "actions.workflow.scope_owner": "Sahibi", + "actions.workflow.scope_global": "Genel", + "actions.workflow.required": "Gerekli", "actions.workflow.run": "İş Akışını Çalıştır", "actions.workflow.not_found": "'%s' iş akışı bulunamadı.", "actions.workflow.run_success": "'%s' iş akışı başarıyla çalıştırıldı.", @@ -3743,5 +3710,6 @@ "git.filemode.normal_file": "Normal dosya", "git.filemode.executable_file": "Çalıştırılabilir dosya", "git.filemode.symbolic_link": "Sembolik Bağlantı", - "git.filemode.submodule": "Alt modül" + "git.filemode.submodule": "Alt modül", + "actions.general.token_permissions.mode.restricted": "Kısıtlanmış" } diff --git a/options/locale/locale_uk-UA.json b/options/locale/locale_uk-UA.json index ac11ba6d47..e7b2eb83e2 100644 --- a/options/locale/locale_uk-UA.json +++ b/options/locale/locale_uk-UA.json @@ -216,7 +216,6 @@ "install.lfs_path": "Кореневий шлях Git LFS", "install.lfs_path_helper": "У цій теці будуть зберігатися файли Git LFS. Залиште порожнім, щоб вимкнути.", "install.run_user": "Виконати як", - "install.run_user_helper": "Ім'я користувача операційної системи, від імені якого запускається Gitea. Зауважте, що цей користувач повинен мати доступ до кореневого шляху сховища.", "install.domain": "Домен сервера", "install.domain_helper": "Домен або хост-адреса сервера.", "install.ssh_port": "Порт SSH сервера", @@ -238,12 +237,6 @@ "install.register_confirm": "Вимагати підтвердження електронною поштою для реєстрації", "install.mail_notify": "Увімкнути сповіщення електронною поштою", "install.server_service_title": "Налаштування сервера і сторонніх сервісів", - "install.offline_mode": "Увімкнути локальний режим", - "install.offline_mode_popup": "Вимкнути сторонні мережі доставки контенту та обслуговувати всі ресурси локально.", - "install.disable_gravatar": "Вимкнути Gravatar", - "install.disable_gravatar_popup": "Вимкнути Gravatar та сторонні джерела аватарок. Якщо користувач локально не завантажить аватар, буде використовуватися типовий аватар.", - "install.federated_avatar_lookup": "Увімкнути зовнішні аватари", - "install.federated_avatar_lookup_popup": "Увімкнути пошук об'єднаних аватарів за допомогою Libravatar.", "install.disable_registration": "Вимкнути реєстрацію", "install.disable_registration_popup": "Вимкнути реєстрацію користувачів, тільки адміністратор може створювати нові облікові записи.", "install.allow_only_external_registration_popup": "Дозволити реєстрацію тільки через сторонні сервіси", @@ -263,12 +256,10 @@ "install.admin_email": "Адреса електронної пошти", "install.install_btn_confirm": "Встановити Gitea", "install.test_git_failed": "Не вдалося перевірити команду 'git': %v", - "install.sqlite3_not_available": "Ця версія Gitea не підтримує SQLite3. Будь ласка, завантажте офіційну бінарну версію з %s (не версію gobuild).", "install.invalid_db_setting": "Налаштування бази даних недійсні: %v", "install.invalid_db_table": "База даних таблиці \"%s\" є недійсною: %v", "install.invalid_repo_path": "Кореневий шлях до сховища невірний: %v", "install.invalid_app_data_path": "Шлях до даних додатка невірний: %v", - "install.run_user_not_match": "Ім'я користувача “Виконати як” не є поточним ім'ям користувача: %s -> %s", "install.internal_token_failed": "Не вдалося створити внутрішній токен: %v", "install.secret_key_failed": "Не вдалося створити секретний ключ: %v", "install.save_config_failed": "Не вдалося зберегти конфігурацію: %v", @@ -554,11 +545,6 @@ "user.block.unblock.failure": "Не вдалося розблокувати користувача: %s", "user.block.blocked": "Ви заблокували цього користувача.", "user.block.title": "Заблокувати користувача", - "user.block.info": "Блокування користувача не дозволяє йому взаємодіяти зі сховищами, наприклад, відкривати або коментувати запити на злиття або задачі. Дізнайтеся більше про блокування користувача.", - "user.block.info_1": "Блокування користувача запобігає наступним діям у вашому обліковому записі та ваших сховищах:", - "user.block.info_2": "слідкують за вашим обліковим записом", - "user.block.info_3": "надсилати вам сповіщення @згадавши ваше ім'я", - "user.block.info_6": "відкриття та коментування задач або запитів на злиття", "user.block.user_to_block": "Блокувати Користувача", "user.block.note": "Примітка", "user.block.note.title": "Необов’язкова примітка:", @@ -775,7 +761,6 @@ "settings.permissions_list": "Дозволи:", "settings.manage_oauth2_applications": "Керування додатками OAuth2", "settings.edit_oauth2_application": "Редагувати додаток OAuth2", - "settings.oauth2_applications_desc": "Додатки OAuth2 дозволяють вашому сторонньому додатку безпечно автентифікувати користувачів у цьому екземплярі Gitea.", "settings.remove_oauth2_application": "Видалити додаток OAuth2", "settings.remove_oauth2_application_desc": "Видалення додатка OAuth2 скасує доступ до всіх підписаних токенів доступу. Продовжити?", "settings.remove_oauth2_application_success": "Додаток видалено.", @@ -792,7 +777,6 @@ "settings.oauth2_regenerate_secret": "Відновити ключ", "settings.oauth2_regenerate_secret_hint": "Втратили ключ?", "settings.oauth2_application_edit": "Редагувати", - "settings.oauth2_application_create_description": "Програми OAuth2 надають вашим стороннім програмам доступ до облікових записів користувачів у цьому екземплярі.", "settings.oauth2_application_remove_description": "Видалення OAuth2 не дозволить додатку отримати доступ до авторизованих облікових записів користувачів на цьому сервері. Продовжити?", "settings.oauth2_application_locked": "Gitea попередньо реєструє деякі програми OAuth2 під час запуску, якщо це ввімкнено в конфігурації. Щоб запобігти несподіваній поведінці, їх не можна ні редагувати, ні видаляти. Для отримання додаткової інформації зверніться до документації OAuth2.", "settings.authorized_oauth2_applications": "Авторизовані програми OAuth2", @@ -860,7 +844,6 @@ "repo.visibility_description": "Тільки власник або члени організації, якщо вони мають дозвіл, зможуть його побачити.", "repo.visibility_helper": "Зробити сховище приватним", "repo.visibility_helper_forced": "Адміністратор вашого сайту налаштував параметри: всі нові репозиторії будуть приватними.", - "repo.visibility_fork_helper": "(Ці зміни вплинуть на всі форки.)", "repo.clone_helper": "Потрібна допомога у клонуванні? Відвідайте сторінку Допомога.", "repo.fork_repo": "Форкнути репозиторій", "repo.fork_from": "Форк з", @@ -941,8 +924,6 @@ "repo.transfer.accept_desc": "Перемістити до \"%s\"", "repo.transfer.reject": "Відхилити переміщення", "repo.transfer.reject_desc": "Скасувати переміщення до \"%s\"", - "repo.transfer.no_permission_to_accept": "У вас немає дозволу приймати цю передачу.", - "repo.transfer.no_permission_to_reject": "У вас немає дозволу на відхилення цієї передачі.", "repo.desc.private": "Приватний", "repo.desc.public": "Публічний", "repo.desc.public_access": "Публічний доступ", @@ -1079,7 +1060,6 @@ "repo.ambiguous_runes_description": "Цей файл містить символи Юнікоду, які можна сплутати з іншими символами. Якщо ви вважаєте, що це зроблено навмисно, можете сміливо ігнорувати це попередження. Щоб показати їх, скористайтеся кнопкою Escape.", "repo.invisible_runes_line": "Цей рядок містить невидимі символи Юнікоду", "repo.ambiguous_runes_line": "Цей рядок містить неоднозначні символи Юнікоду", - "repo.ambiguous_character": "%[1]c [U+%04[1]X] можна сплутати з %[2]c [U+%04[2]X]", "repo.escape_control_characters": "Екранувати", "repo.unescape_control_characters": "Відмінити екранування", "repo.file_copy_permalink": "Копіювати постійне посилання", @@ -1155,7 +1135,6 @@ "repo.editor.unable_to_upload_files": "Не вдалося завантажити файли до '\"%s\". Помилка: %v", "repo.editor.upload_file_is_locked": "Файл \"%s\" заблоковано %s.", "repo.editor.upload_files_to_dir": "Завантажити файли до \"%s\"", - "repo.editor.no_commit_to_branch": "Не вдалося внести коміт безпосередньо до гілки, тому що:", "repo.editor.require_signed_commit": "Гілка вимагає підписаного коміту", "repo.editor.revert": "Повернути %s до:", "repo.editor.failed_to_commit": "Не вдалося зафіксувати зміни.", @@ -1163,7 +1142,6 @@ "repo.commits.desc": "Переглянути історію зміни коду.", "repo.commits.commits": "Коміти", "repo.commits.no_commits": "Немає спільних комітів. '%s' та '%s' мають різну історію.", - "repo.commits.nothing_to_compare": "Ці гілки однакові.", "repo.commits.search_branch": "Ця гілка", "repo.commits.search_all": "Усі гілки", "repo.commits.author": "Автор", @@ -1233,11 +1211,9 @@ "repo.issues.new": "Нова задача", "repo.issues.new.title_empty": "Заголовок не може бути порожнім", "repo.issues.new.labels": "Мітки", - "repo.issues.new.no_label": "Без мітки", "repo.issues.new.clear_labels": "Очистити мітки", "repo.issues.new.projects": "Проєкти", "repo.issues.new.clear_projects": "Скинути проєкти", - "repo.issues.new.no_projects": "Проєкт відсутній", "repo.issues.new.open_projects": "Відкриті проєкти", "repo.issues.new.closed_projects": "Закриті проєкти", "repo.issues.new.no_items": "Немає елементів", @@ -1358,7 +1334,6 @@ "repo.issues.comment_pull_merged_at": "об'єднав(-ла) коміти %[1]s в %[2]s %[3]s", "repo.issues.comment_manually_pull_merged_at": "вручну об'єднав(-ла) коміти %[1]s в %[2]s %[3]s", "repo.issues.close_comment_issue": "Закрити з коментарем", - "repo.issues.reopen_issue": "Відкрити знову", "repo.issues.reopen_comment_issue": "Повторно відкрити з коментарем", "repo.issues.create_comment": "Коментар", "repo.issues.comment.blocked_user": "Неможливо створити або редагувати коментар, тому що ви заблоковані автором або власником сховища.", @@ -1565,7 +1540,6 @@ "repo.pulls.compare_changes": "Новий запит на злиття", "repo.pulls.allow_edits_from_maintainers_desc": "Користувачі з доступом на запис до базової гілки також можуть завантажувати свої зміни до цієї гілки", "repo.pulls.allow_edits_from_maintainers_err": "Оновлення не вдалося", - "repo.pulls.compare_changes_desc": "Порівняти дві гілки і створити запит на злиття для змін.", "repo.pulls.has_viewed_file": "Переглядів", "repo.pulls.has_changed_since_last_review": "Змінено з моменту вашого останнього відгуку", "repo.pulls.viewed_files_label": "Переглянуто %[1]d / %[2]d файлів", @@ -1580,7 +1554,6 @@ "repo.pulls.showing_only_single_commit": "Відображаються лише зміни коміту %[1]s", "repo.pulls.showing_specified_commit_range": "Відображаються лише зміни між %[1]s..%[2]s", "repo.pulls.filter_changes_by_commit": "Фільтр за комітом", - "repo.pulls.nothing_to_compare": "Ці гілки однакові. Немає необхідності створювати запитів на злиття.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "Одинакові гілки. Цей PR буде порожнім.", "repo.pulls.has_pull_request": "Запит злиття для цих гілок вже існує: %[2]s#%[3]d", "repo.pulls.create": "Створити запит на злиття", @@ -1637,7 +1610,6 @@ "repo.pulls.status_checking": "Деякі перевірки ще не завершені", "repo.pulls.status_checks_success": "Всі перевірки були успішними", "repo.pulls.status_checks_warning": "Декілька перевірок завершилися з попередженнями", - "repo.pulls.status_checks_failure": "Деякі перевірки не спрацювали", "repo.pulls.status_checks_error": "Декілька перевірок завершилися з помилками", "repo.pulls.status_checks_requested": "Обов'язково", "repo.pulls.status_checks_details": "Подробиці", @@ -1703,7 +1675,6 @@ "repo.signing.wont_sign.headsigned": "Об'єднання не буде підписане, оскільки не підписано головний коміт.", "repo.signing.wont_sign.commitssigned": "Об'єднання не буде підписане, оскільки всі пов'язані з ним коміти не підписані.", "repo.signing.wont_sign.approved": "Об'єднання не буде підписане, оскільки злиття не затверджено.", - "repo.signing.wont_sign.not_signed_in": "Ви не увійшли до системи.", "repo.ext_wiki": "Доступ до зовнішньої вікі", "repo.ext_wiki.desc": "Посилання на зовнішню вікі.", "repo.wiki": "Вікі", @@ -1901,7 +1872,6 @@ "repo.settings.transfer_abort_invalid": "Ви не можете скасувати перенесення неіснуючого сховища.", "repo.settings.transfer_abort_success": "Перенесення сховища до %s успішно скасовано.", "repo.settings.transfer_desc": "Передати це сховище користувачеві або організації, для якої ви маєте права адміністратора.", - "repo.settings.transfer_form_title": "Введіть назву сховища для підтвердження:", "repo.settings.transfer_notices_1": "- Ви втратите доступ до сховища, якщо передасте його окремому користувачеві.", "repo.settings.transfer_notices_2": "- Ви збережете доступ до сховища, якщо передасте його організації, якою ви (спів)володієте.", "repo.settings.transfer_notices_3": "- Якщо сховище є приватним і передається окремому користувачеві, ця дія гарантує, що користувач має принаймні права на читання (і змінює ці права, якщо необхідно).", @@ -1961,8 +1931,6 @@ "repo.settings.webhook.body": "Тіло", "repo.settings.webhook.replay.description": "Повторити цей веб-хук.", "repo.settings.githook_edit_desc": "Якщо хук неактивний, буде показано зразок вмісту. Якщо залишити вміст порожнім, хук буде вимкнено.", - "repo.settings.githook_name": "Назва хуку", - "repo.settings.githook_content": "Зміст хука", "repo.settings.update_githook": "Оновити хук", "repo.settings.add_webhook_desc": "Gitea надішле запити POST із зазначеним типом змісту на цільову URL-адресу. Дізнайтеся більше в інструкції по використанню веб-хуків.", "repo.settings.payload_url": "Цільова URL-адреса", @@ -2087,9 +2055,7 @@ "repo.settings.block_outdated_branch": "Блокувати об'єднання, якщо запит на злиття застарів", "repo.settings.block_outdated_branch_desc": "Об'єднання буде неможливим, якщо головна гілка позаду основної.", "repo.settings.block_admin_merge_override": "Адміністратори повинні дотримуватися правил захисту гілки", - "repo.settings.default_branch_desc": "Обрати типову гілку сховища для запитів на злиття і комітів:", "repo.settings.merge_style_desc": "Стилі об'єднання", - "repo.settings.default_merge_style_desc": "Типовий стиль об'єднання", "repo.settings.choose_branch": "Оберіть гілку…", "repo.settings.no_protected_branch": "Немає захищених гілок.", "repo.settings.edit_protected_branch": "Редагувати", @@ -2176,8 +2142,7 @@ "repo.diff.too_many_files": "Деякі з файлів не показано, оскільки було змінено дуже багато файлів", "repo.diff.show_more": "Показати більше", "repo.diff.load": "Завантажити різницю", - "repo.diff.generated": "згенерований", - "repo.diff.vendored": "сторонній", + "repo.diff.generated": "Створено", "repo.diff.comment.add_line_comment": "Додати простий коментар", "repo.diff.comment.placeholder": "Залишити коментар", "repo.diff.comment.add_single_comment": "Додати простий коментар", @@ -2191,6 +2156,7 @@ "repo.diff.review.approve": "Затвердити", "repo.diff.review.reject": "Запит змін", "repo.diff.committed_by": "зафіксовано", + "repo.commits.avatar_stack_and": "та", "repo.diff.protected": "Захищений", "repo.diff.image.side_by_side": "Поруч", "repo.diff.image.swipe": "Провести пальцем", @@ -2263,7 +2229,6 @@ "repo.branch.restore_success": "Гілку \"%s\" відновлено.", "repo.branch.restore_failed": "Не вдалося відновити гілку \"%s\".", "repo.branch.protected_deletion_failed": "Гілка \"%s\" захищена. Її неможливо видалити.", - "repo.branch.default_deletion_failed": "Гілка \"%s\" стандартна. Її неможливо видалити.", "repo.branch.restore": "Відновити гілку \"%s\"", "repo.branch.download": "Завантажити гілку \"%s\"", "repo.branch.rename": "Перейменувати гілку \"%s\"", @@ -2279,7 +2244,6 @@ "repo.branch.new_branch_from": "Створити нову гілку з \"%s\"", "repo.branch.renamed": "Гілку %s перейменовано на %s.", "repo.branch.rename_default_or_protected_branch_error": "Лише адміністратори можуть перейменовувати типові або захищені гілки.", - "repo.branch.rename_protected_branch_failed": "Ця гілка захищена правилами захисту на основі глобальних правил.", "repo.tag.create_tag": "Створити тег %s", "repo.tag.create_tag_operation": "Створити мітку", "repo.tag.confirm_create_tag": "Створити мітку", @@ -2358,9 +2322,7 @@ "org.settings.labels_desc": "Додайте мітки, які можна використовувати у задачах для усіх сховищ у цій організації.", "org.members.membership_visibility": "Видимість учасника:", "org.members.public": "Показувати", - "org.members.public_helper": "приховати", "org.members.private": "Прихований", - "org.members.private_helper": "зробити видимим", "org.members.member_role": "Роль учасника:", "org.members.owner": "Власник", "org.members.member": "Учасник", @@ -2391,7 +2353,6 @@ "org.teams.members": "Учасники команди", "org.teams.update_settings": "Оновити налаштування", "org.teams.delete_team": "Видалити команду", - "org.teams.add_team_member": "Додати учасника команди", "org.teams.invite_team_member": "Запросити до %s", "org.teams.delete_team_title": "Видалити команду", "org.teams.delete_team_desc": "Видалення команди скасовує доступ до репозиторія для її учасників. Продовжити?", @@ -2416,6 +2377,10 @@ "org.teams.all_repositories_read_permission_desc": "Ця команда надає дозвіл Перегляд для всіх репозиторіїв: учасники можуть переглядати та клонувати їх.", "org.teams.all_repositories_write_permission_desc": "Ця команда надає дозвіл Запис для всіх репозиторіїв: учасники можуть переглядати та виконувати push в репозиторіях.", "org.teams.all_repositories_admin_permission_desc": "Ця команда надає дозвіл Адміністрування для всіх репозиторіїв: учасники можуть переглядати, виконувати push та додавати співробітників.", + "org.teams.visibility": "Видимість", + "org.teams.visibility_private": "Приватний", + "org.teams.visibility_limited": "Обмежений", + "org.teams.visibility_public": "Публічна", "org.teams.invite.title": "Вас запросили приєднатися до команди %s в організації %s.", "org.teams.invite.by": "Запрошений %s", "org.teams.invite.description": "Будь ласка, натисніть на кнопку нижче, щоб приєднатися до команди.", @@ -2440,6 +2405,7 @@ "admin.hooks": "Веб-хуки", "admin.integrations": "Інтеграції", "admin.authentication": "Джерела автентифікації", + "admin.badges.description": "Опис", "admin.config": "Конфігурація", "admin.config_summary": "Підсумок", "admin.config_settings": "Налаштування", @@ -2715,11 +2681,8 @@ "admin.config.server_config": "Налаштування сервера", "admin.config.app_name": "Назва сайту", "admin.config.app_ver": "Версія Gitea", - "admin.config.app_url": "Базова URL-адреса Gitea", "admin.config.custom_conf": "Шлях до файлу конфігурації", "admin.config.custom_file_root_path": "Шлях до файлу користувача", - "admin.config.domain": "Домен сервера", - "admin.config.offline_mode": "Локальний режим", "admin.config.disable_router_log": "Вимкнути журнал маршрутизатора", "admin.config.run_user": "Запуск від імені Користувача", "admin.config.run_mode": "Режим виконання", @@ -2769,7 +2732,6 @@ "admin.config.default_enable_timetracking": "Увімкнути відстеження часу за замовчуванням", "admin.config.default_allow_only_contributors_to_track_time": "Враховувати тільки учасників розробки в підрахунку часу", "admin.config.no_reply_address": "Прихований домен електронної пошти", - "admin.config.default_visibility_organization": "Видимість за замовчуванням для нових організацій", "admin.config.default_enable_dependencies": "Увімкнути залежності задачі за замовчуванням", "admin.config.webhook_config": "Конфігурація web-хуків", "admin.config.queue_length": "Довжина черги", @@ -2797,7 +2759,6 @@ "admin.config.cache_config": "Конфігурація кешу", "admin.config.cache_adapter": "Адаптер кешу", "admin.config.cache_interval": "Інтервал кешування", - "admin.config.cache_conn": "Підключення до кешу", "admin.config.cache_item_ttl": "Час зберігання даних кешу", "admin.config.cache_test": "Перевірити кеш", "admin.config.cache_test_failed": "Не вдалося перевірити кеш: %v.", @@ -2805,7 +2766,6 @@ "admin.config.cache_test_succeeded": "Тест кешу успішно завершено, отримано відповідь за %s.", "admin.config.session_config": "Конфігурація сесії", "admin.config.session_provider": "Провайдер сесії", - "admin.config.provider_config": "Конфігурація постачальника", "admin.config.cookie_name": "Ім'я файлу cookie", "admin.config.gc_interval_time": "Інтервал запуску збирача сміття (GC)", "admin.config.session_life_time": "Час життя сесії", @@ -2813,7 +2773,6 @@ "admin.config.cookie_life_time": "Час життя cookie-файлу", "admin.config.picture_config": "Налаштування фотографії або аватари", "admin.config.picture_service": "Сервіс зображень", - "admin.config.disable_gravatar": "Вимкнути Gravatar", "admin.config.enable_federated_avatar": "Увімкнути зовнішні аватари", "admin.config.git_config": "Конфігурація git", "admin.config.git_disable_diff_highlight": "Вимкнути підсвітку синтаксису diff", @@ -2823,8 +2782,6 @@ "admin.config.git_gc_args": "Аргументи збирача сміття (GC)", "admin.config.git_migrate_timeout": "Тайм-аут міграції", "admin.config.git_mirror_timeout": "Тайм-аут оновлення дзеркала", - "admin.config.git_clone_timeout": "Тайм-аут операції клонування", - "admin.config.git_pull_timeout": "Тайм-аут операції Pull", "admin.config.git_gc_timeout": "Тайм-аут операції збирача сміття (GC)", "admin.config.log_config": "Конфігурація журналу", "admin.config.logger_name_fmt": "Журнал: %s", @@ -3050,6 +3007,10 @@ "packages.rubygems.required.rubygems": "Вимагає версію RubyGem", "packages.swift.install": "Додайте пакет у ваш файл Package.swift:", "packages.swift.install2": "і виконайте наступну команду:", + "packages.terraform.install2": "і виконайте наступну команду:", + "packages.terraform.lock_status": "Статус блокування", + "packages.terraform.lock": "Заблокувати", + "packages.terraform.unlock": "Розблокувати", "packages.vagrant.install": "Щоб додати бокс Vagrant, виконайте наступну команду:", "packages.settings.link": "Прив'язати пакет до сховища", "packages.settings.link.select": "Обрати сховище", @@ -3133,6 +3094,8 @@ "actions.runs.status": "Статус", "actions.runs.actors_no_select": "Усі актори", "actions.runs.status_no_select": "Всі статуси", + "actions.runs.branch": "Гілка", + "actions.runs.branches_no_select": "Усі гілки", "actions.runs.no_results": "Збігів немає.", "actions.runs.no_workflows": "Робочих процесів наразі немає.", "actions.runs.no_workflows.quick_start": "Не знаєте, як почати з Gitea Дії? Дивіться посібник швидкого старту.", @@ -3144,11 +3107,15 @@ "actions.runs.delete.description": "Ви впевнені, що хочете остаточно видалити цей робочий процес? Цю дію неможливо скасувати.", "actions.runs.not_done": "Виконання цього робочого процесу не завершено.", "actions.runs.view_workflow_file": "Перегляд файлу робочого процесу", + "actions.runs.summary": "Підсумок", + "actions.runs.latest": "Останні", "actions.workflow.disable": "Вимкнути робочий процес", "actions.workflow.disable_success": "Робочий процес '%s' успішно вимкнено.", "actions.workflow.enable": "Увімкнути робочий процес", "actions.workflow.enable_success": "Робочий процес '%s' успішно ввімкнено.", "actions.workflow.disabled": "Робочий процес вимкнений.", + "actions.workflow.scope_owner": "Власник", + "actions.workflow.required": "Обов'язково", "actions.workflow.run": "Запустити робочий процес", "actions.workflow.not_found": "Робочий процес '%s' не знайдено.", "actions.workflow.run_success": "Робочий процес '%s' завершився успішно.", @@ -3179,8 +3146,6 @@ "projects.enter_fullscreen": "Повноекранний режим", "projects.exit_fullscreen": "Вийти з повноекранного режиму", "git.filemode.directory": "Тека", - "git.filemode.normal_file": "Звичайний файл", - "git.filemode.executable_file": "Виконуваний файл", - "git.filemode.symbolic_link": "Символічне посилання", - "git.filemode.submodule": "Підмодуль" + "git.filemode.submodule": "Підмодуль", + "actions.general.token_permissions.mode.restricted": "Обмежено" } diff --git a/options/locale/locale_zh-CN.json b/options/locale/locale_zh-CN.json index 7c3d45f528..de339ce5c2 100644 --- a/options/locale/locale_zh-CN.json +++ b/options/locale/locale_zh-CN.json @@ -2251,7 +2251,6 @@ "repo.settings.webhook_deletion_success": "Web 钩子删除成功!", "repo.settings.webhook.test_delivery": "测试推送事件", "repo.settings.webhook.test_delivery_desc": "用假推送事件测试这个 Web 钩子。", - "repo.settings.webhook.test_delivery_desc_disabled": "要用假事件测试这个 Web钩子,请激活它。", "repo.settings.webhook.request": "请求内容", "repo.settings.webhook.response": "响应内容", "repo.settings.webhook.headers": "头信息", @@ -3306,7 +3305,6 @@ "admin.config.default_enable_timetracking": "默认情况下启用时间跟踪", "admin.config.default_allow_only_contributors_to_track_time": "仅允许成员跟踪时间", "admin.config.no_reply_address": "隐藏邮件域", - "admin.config.default_visibility_organization": "新组织的默认可见性", "admin.config.default_enable_dependencies": "默认情况下启用工单依赖", "admin.config.webhook_config": "Web 钩子配置", "admin.config.queue_length": "队列长度", @@ -3779,6 +3777,7 @@ "actions.runs.commit": "提交", "actions.runs.run_details": "运行详情", "actions.runs.workflow_file": "工作流文件", + "actions.runs.workflow_file_no_permission": "没有权限查看工作流文件", "actions.runs.scheduled": "已计划的", "actions.runs.pushed_by": "推送者", "actions.runs.invalid_workflow_helper": "工作流配置文件无效。请检查您的配置文件:%s", @@ -3832,7 +3831,36 @@ "actions.workflow.enable": "启用工作流", "actions.workflow.enable_success": "工作流「%s」已成功启用。", "actions.workflow.disabled": "工作流已禁用。", + "actions.workflow.scope_owner": "拥有者", + "actions.workflow.scope_global": "全局", + "actions.workflow.required": "必须", + "actions.workflow.scoped_required_cannot_disable": "这个作用域工作流是必需的,不能被禁用。", + "actions.scoped_workflows": "作用域工作流", + "actions.scoped_workflows.desc_org": "将仓库注册为作用域工作流的源仓库。源仓库默认分支的「作用域工作流目录」中的工作流文件会在此组织中的每个仓库上运行,并使用对应仓库自身的上下文。", + "actions.scoped_workflows.desc_user": "将仓库注册为作用域工作流的源仓库。源仓库默认分支的「作用域工作流目录」中的工作流文件会在您拥有的每个仓库上运行,并使用对应仓库自身的上下文。", + "actions.scoped_workflows.desc_global": "将仓库注册为作用域工作流的源仓库。源仓库默认分支的「作用域工作流目录」中的工作流文件会在这个实例的每个仓库上运行,并使用对应仓库自身的上下文。由于实例级注册会针对每个仓库的事件进行检测,这在大型实例上可能会带来额外开销。", + "actions.scoped_workflows.add_help": "若要从仓库中提供作用域工作流,请在默认分支上提交工作流文件至 %s 然后将仓库注册为下面的源仓库。", + "actions.scoped_workflows.security_note": "一个源仓库的工作流会在它应用到的每个仓库上执行,它的 step 脚本和输出会被写到相关仓库的工作流日志里,并且能被任何有权查看这个仓库工作流的用户读取。因此,将一个私有仓库注册为源仓库会通过这些日志暴露工作流的逻辑。请仅注册那些工作流内容可以与所有相关仓库共享的仓库。如果一个作用域工作流从一个私有仓库引用了可复用工作流,请确保每个相关仓库都能读取它,否则该工作流会在这些仓库中运行失败。", + "actions.scoped_workflows.source.add": "添加源仓库", + "actions.scoped_workflows.source.add_success": "源仓库已添加。", + "actions.scoped_workflows.source.remove_success": "源仓库已删除。", + "actions.scoped_workflows.source.not_found": "未找到仓库。", + "actions.scoped_workflows.required.update_success": "「必需」工作流已更新。", + "actions.scoped_workflows.required.label": "把工作流标记为必需(「必需」工作流不能被仓库禁用):", + "actions.scoped_workflows.required.patterns": "「必需」状态检查表达式", + "actions.scoped_workflows.required.patterns_aria": "%s 的「必需」状态检查表达式", + "actions.scoped_workflows.required.patterns_note": "仅在工作流为「必需」时执行", + "actions.scoped_workflows.required.patterns_hint": "标记工作流为「必需」以配置它的状态检查表达式。", + "actions.scoped_workflows.required.patterns_help": "每行一个状态检查表达式(glob)。消费了这个工作流的合并请求只能在每条表达式都通过后才能合并。这仅适用于目标分支被分支保护规则保护的情况,即使分支保护规则的状态检查被禁用。没有被分支保护规则保护的目标分支不会被限制。\n每行填写一个状态检查表达式(glob)。只有当每个表达式都有一个匹配且已通过的状态检查时,相关的合并请求才能被合并。这仅适用于目标分支被分支保护规则保护的情况,即使分支保护规则的状态检查被禁用。没有被分支保护规则保护的目标分支不会被限制。", + "actions.scoped_workflows.required.patterns_empty": "每个「必需」工作流都需要至少一个状态检查表达式。", + "actions.scoped_workflows.required.missing_file": "文件不存在于源仓库", + "actions.scoped_workflows.required.expected_contexts": "预期状态检查(匹配表达式的状态检查会被标记)", + "actions.scoped_workflows.required.no_status_contexts": "此工作流不会报告任何状态检查,将其标记为「必需」后所有相关的合并请求都将无法合并。请不要将其标记为「必需」。", + "actions.scoped_workflows.no_files": "默认分支上未找到任何作用域工作流文件。", "actions.workflow.run": "运行工作流", + "actions.workflow.create_status_badge": "创建状态徽章", + "actions.workflow.status_badge": "状态徽章", + "actions.workflow.status_badge_url": "徽章 URL", "actions.workflow.not_found": "未找到工作流「%s」。", "actions.workflow.run_success": "工作流「%s」已成功运行。", "actions.workflow.from_ref": "使用工作流从", diff --git a/options/locale/locale_zh-TW.json b/options/locale/locale_zh-TW.json index 93c942aa05..6729892c9d 100644 --- a/options/locale/locale_zh-TW.json +++ b/options/locale/locale_zh-TW.json @@ -186,7 +186,6 @@ "startpage.lightweight": "輕量級", "startpage.lightweight_desc": "一片便宜的 Raspberry Pi 就可以滿足 Gitea 的最低需求。節省您的機器資源!", "startpage.license": "開放原始碼", - "startpage.license_desc": "取得 code.gitea.io/gitea !成為一名貢獻者和我們一起讓 Gitea 更好,快點加入我們吧!", "install.install": "安裝頁面", "install.title": "初始組態", "install.docker_helper": "如果您在 Docker 中執行 Gitea,請先閱讀安裝指南再來調整設定。", @@ -214,7 +213,6 @@ "install.lfs_path": "Git LFS 根目錄", "install.lfs_path_helper": "以 Git LFS 儲存檔案時會被儲存在此目錄中。請留空以停用 LFS 功能。", "install.run_user": "以使用者名稱執行", - "install.run_user_helper": "輸入 Gitea 執行的作業系統使用者名稱。請注意,此使用者必須擁有存儲庫根目錄的存取權限。", "install.domain": "伺服器域名", "install.domain_helper": "伺服器的域名或主機位置。", "install.ssh_port": "SSH 伺服器埠", @@ -236,12 +234,6 @@ "install.register_confirm": "要求註冊時確認電子郵件", "install.mail_notify": "啟用郵件通知", "install.server_service_title": "伺服器和第三方服務設定", - "install.offline_mode": "啟用本地模式", - "install.offline_mode_popup": "停用其他服務並在本地提供所有資源。", - "install.disable_gravatar": "停用 Gravatar", - "install.disable_gravatar_popup": "停用 Gravatar 和其他大頭貼服務。除非使用者在本地上傳大頭貼,否則將使用預設的大頭貼。", - "install.federated_avatar_lookup": "啟用 Federated Avatars", - "install.federated_avatar_lookup_popup": "使用 Libravatar 以啟用 Federated Avatar 查詢服務", "install.disable_registration": "關閉註冊功能", "install.disable_registration_popup": "關閉註冊功能,只有管理員可以新增帳戶。", "install.allow_only_external_registration_popup": "只允許從外部服務註冊", @@ -261,12 +253,10 @@ "install.admin_email": "電子信箱", "install.install_btn_confirm": "安裝 Gitea", "install.test_git_failed": "無法識別「git」命令:%v", - "install.sqlite3_not_available": "您目前的版本不支援 SQLite3,請從 %s 下載官方的預先編譯版本 (不是 gobuild 版本)。", "install.invalid_db_setting": "資料庫設定不正確: %v", "install.invalid_db_table": "資料庫的資料表「%s」無效: %v", "install.invalid_repo_path": "儲存庫根目錄設定不正確:%v", "install.invalid_app_data_path": "無效的應用程式資料路徑:%v", - "install.run_user_not_match": "「以...執行」的使用者名稱不是目前的使用者名稱:%s -> %s", "install.internal_token_failed": "產生內部 Token 失敗:%v", "install.secret_key_failed": "產生密鑰失敗:%v", "install.save_config_failed": "儲存設定失敗:%v", @@ -546,13 +536,6 @@ "user.block.unblock.failure": "無法解除封鎖使用者: %s", "user.block.blocked": "您已封鎖該使用者。", "user.block.title": "封鎖使用者", - "user.block.info": "阻擋使用者可避免他們與儲存庫互動,如在建立合併請求或是問題以及於其上留言。了解更多關於阻擋一個使用者。", - "user.block.info_1": "阻擋一個使用者可避免其對您的帳號或是儲存庫進行以下動作:", - "user.block.info_2": "正在追蹤你的帳戶", - "user.block.info_3": "透過 @mentioning 標記您的使用者名稱來通知您", - "user.block.info_4": "邀請您成為他們的儲存庫的協作者", - "user.block.info_5": "對儲存庫加上星號、建立分之或是關注儲存庫", - "user.block.info_6": "建立問題或是合併請求,或是對其進行留言", "user.block.user_to_block": "欲阻擋的使用者", "user.block.note": "備註", "user.block.note.title": "選用附註:", @@ -763,7 +746,6 @@ "settings.permissions_list": "權限:", "settings.manage_oauth2_applications": "管理 OAuth2 應用程式", "settings.edit_oauth2_application": "編輯 OAuth2 應用程式", - "settings.oauth2_applications_desc": "OAuth2 應用程式讓您的第三方應用程式安全地驗證此 Gitea 中的使用者。", "settings.remove_oauth2_application": "刪除 OAuth2 應用程式", "settings.remove_oauth2_application_desc": "刪除 OAuth2 應用程式將會撤銷所有已簽署的 Access Token 存取權。是否繼續?", "settings.remove_oauth2_application_success": "已刪除應用程式。", @@ -781,7 +763,6 @@ "settings.oauth2_regenerate_secret_hint": "遺失您的密鑰?", "settings.oauth2_client_secret_hint": "離開或重新整理此頁面後將不會再顯示密鑰。請確保您已保存它。", "settings.oauth2_application_edit": "編輯", - "settings.oauth2_application_create_description": "OAuth2 應用程式讓您的第三方應用程式可以存取此 Gitea 上的帳戶。", "settings.oauth2_application_remove_description": "移除 OAuth2 應用程式將阻止其存取此實例上的授權使用者帳戶。是否繼續?", "settings.oauth2_application_locked": "Gitea 在啟動時會預先註冊一些 OAuth2 應用程式(如果在配置中啟用)。為防止意外行為,這些應用程式無法編輯或刪除。請參閱 OAuth2 文件以獲取更多資訊。", "settings.authorized_oauth2_applications": "已授權的 OAuth2 應用程式", @@ -851,7 +832,6 @@ "repo.visibility_description": "只有組織擁有者或有權限的組織成員才能看到。", "repo.visibility_helper": "將儲存庫設為私有", "repo.visibility_helper_forced": "您的網站管理員強制新的存儲庫必需設定為私有。", - "repo.visibility_fork_helper": "(修改本值將會影響所有 fork 儲存庫)", "repo.clone_helper": "需要有關 Clone 的協助嗎?查看幫助 。", "repo.fork_repo": "Fork 儲存庫", "repo.fork_from": "Fork 自", @@ -937,8 +917,6 @@ "repo.transfer.accept_desc": "轉移到「%s」", "repo.transfer.reject": "拒絕轉移", "repo.transfer.reject_desc": "取消轉移到「%s」", - "repo.transfer.no_permission_to_accept": "您沒有權限接受此轉移。", - "repo.transfer.no_permission_to_reject": "您沒有權限拒絕此轉移。", "repo.desc.private": "私有", "repo.desc.public": "公開", "repo.desc.template": "模板", @@ -1071,7 +1049,6 @@ "repo.ambiguous_runes_description": "此檔案包含可能與其他字元混淆的 Unicode 字元。如果您認為這是有意的,可以安全地忽略此警告。使用 Escape 鍵來顯示它們。", "repo.invisible_runes_line": "這一行有看不見的 Unicode 字元", "repo.ambiguous_runes_line": "這一行有易混淆的 Unicode 字元", - "repo.ambiguous_character": "%[1]c [U+%04[1]X] 容易與 %[2]c [U+%04[2]X] 混淆", "repo.escape_control_characters": "轉義控制字元", "repo.unescape_control_characters": "取消轉義控制字元", "repo.file_copy_permalink": "複製固定連結", @@ -1151,7 +1128,6 @@ "repo.editor.upload_file_is_locked": "檔案「%s」已被 %s 鎖定。", "repo.editor.upload_files_to_dir": "上傳檔案到「%s」", "repo.editor.cannot_commit_to_protected_branch": "無法提交到受保護的分支「%s」。", - "repo.editor.no_commit_to_branch": "無法直接提交到分支因為:", "repo.editor.user_no_push_to_branch": "使用者無法推送到分支", "repo.editor.require_signed_commit": "分支僅接受經簽署的提交", "repo.editor.cherry_pick": "Cherry-pick %s 到:", @@ -1159,7 +1135,6 @@ "repo.commits.desc": "瀏覽原始碼修改歷程。", "repo.commits.commits": "次程式碼提交", "repo.commits.no_commits": "沒有共同的提交。「%s」和「%s」的歷史完全不同。", - "repo.commits.nothing_to_compare": "這些分支是相同的。", "repo.commits.search.tooltip": "你可以用「author:」、「committer:」、「after:」、「before:」等作為關鍵字的前綴,例如: 「revert author:Alice before:2019-01-13」。", "repo.commits.search_branch": "此分支", "repo.commits.search_all": "所有分支", @@ -1230,11 +1205,9 @@ "repo.issues.new": "新增問題", "repo.issues.new.title_empty": "標題不可為空", "repo.issues.new.labels": "標籤", - "repo.issues.new.no_label": "未選擇標籤", "repo.issues.new.clear_labels": "清除已選取標籤", "repo.issues.new.projects": "專案", "repo.issues.new.clear_projects": "清除已選取專案", - "repo.issues.new.no_projects": "未選擇專案", "repo.issues.new.open_projects": "開放中的專案", "repo.issues.new.closed_projects": "已關閉的專案", "repo.issues.new.no_items": "沒有項目", @@ -1357,7 +1330,6 @@ "repo.issues.comment_pull_merged_at": "合併提交 %[1]s 到 %[2]s %[3]s", "repo.issues.comment_manually_pull_merged_at": "手動合併提交 %[1]s 到 %[2]s %[3]s", "repo.issues.close_comment_issue": "留言並關閉", - "repo.issues.reopen_issue": "重新開放", "repo.issues.reopen_comment_issue": "留言並重新開放", "repo.issues.create_comment": "留言", "repo.issues.comment.blocked_user": "無法建立或編輯留言,因為您被發文者或儲存庫擁有者封鎖。", @@ -1561,7 +1533,6 @@ "repo.pulls.allow_edits_from_maintainers": "允許維護者編輯", "repo.pulls.allow_edits_from_maintainers_desc": "對基礎分支有寫入權限的使用者也可以推送到此分支", "repo.pulls.allow_edits_from_maintainers_err": "更新失敗", - "repo.pulls.compare_changes_desc": "選擇合併的目標分支和來源分支。", "repo.pulls.has_viewed_file": "已檢視", "repo.pulls.has_changed_since_last_review": "您上次審核後有變動", "repo.pulls.viewed_files_label": "%[1]d / %[2]d 個檔案已檢視", @@ -1578,7 +1549,6 @@ "repo.pulls.showing_specified_commit_range": "僅顯示介於 %[1]s 和 %[2]s 之間的變更", "repo.pulls.review_only_possible_for_full_diff": "僅在查看完整差異時才能進行審核", "repo.pulls.filter_changes_by_commit": "按提交篩選變更", - "repo.pulls.nothing_to_compare": "這些分支的內容相同,無需建立合併請求。", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "這些分支的內容相同,此合併請求將會是空白的。", "repo.pulls.has_pull_request": "已有介於這些分支間的合併請求:%[2]s#%[3]d", "repo.pulls.create": "建立合併請求", @@ -1644,7 +1614,6 @@ "repo.pulls.status_checking": "還在進行一些檢查", "repo.pulls.status_checks_success": "已通過所有檢查", "repo.pulls.status_checks_warning": "一些檢查回報了警告", - "repo.pulls.status_checks_failure": "一些檢查失敗了", "repo.pulls.status_checks_error": "一些檢查回報了錯誤", "repo.pulls.status_checks_requested": "必要", "repo.pulls.status_checks_details": "詳情", @@ -1720,7 +1689,6 @@ "repo.signing.wont_sign.headsigned": "合併不會被簽署,因為 head 提交未簽署。", "repo.signing.wont_sign.commitssigned": "合併不會被簽署,因為所有相關的提交都未簽署。", "repo.signing.wont_sign.approved": "合併不會被簽署,因為 PR 未被核准。", - "repo.signing.wont_sign.not_signed_in": "你還沒有登入。", "repo.ext_wiki": "存取外部 Wiki", "repo.ext_wiki.desc": "連結外部 Wiki。", "repo.wiki.welcome": "歡迎使用 Wiki!", @@ -1930,7 +1898,6 @@ "repo.settings.transfer_abort_invalid": "您無法取消不存在的儲存庫轉移。", "repo.settings.transfer_abort_success": "成功取消轉移儲存庫至 %s。", "repo.settings.transfer_desc": "將此儲存庫轉移給其他使用者或受您管理的組織。", - "repo.settings.transfer_form_title": "輸入儲存庫名稱以確認:", "repo.settings.transfer_notices_1": "- 如果將此儲存庫轉移給個別使用者,您將會失去此儲存庫的存取權。", "repo.settings.transfer_notices_2": "- 如果將此儲存庫轉移到您(共同)擁有的組織,您將能繼續保有此儲存庫的存取權。", "repo.settings.transfer_notices_3": "- 如果此儲存庫為私有儲存庫且將轉移給個別使用者,此動作確保該使用者至少擁有讀取權限 (必要時將會修改權限)。", @@ -1985,7 +1952,6 @@ "repo.settings.webhook_deletion": "移除 Webhook", "repo.settings.webhook_deletion_desc": "移除 Webhook 將刪除它的設定及傳送記錄,是否繼續?", "repo.settings.webhook_deletion_success": "Webhook 已移除。", - "repo.settings.webhook.test_delivery_desc_disabled": "要使用假事件測試此 Webhook,請啟用它。", "repo.settings.webhook.request": "請求", "repo.settings.webhook.response": "回應", "repo.settings.webhook.headers": "標頭", @@ -1996,8 +1962,6 @@ "repo.settings.webhook.delivery.success": "已將事件加入到傳送佇列,可能需要等待幾分鐘才會出現於傳送紀錄。", "repo.settings.githooks_desc": "Git Hook 是 Git 本身提供的功能。您可以在下方編輯 hook 檔案以設定自訂作業。", "repo.settings.githook_edit_desc": "如果 Hook 未啟動,則會顯示範例文件中的內容。如果想要刪除某個 Hook,則送出空白內容即可。", - "repo.settings.githook_name": "Hook 名稱", - "repo.settings.githook_content": "Hook 內容", "repo.settings.update_githook": "更新 Hook", "repo.settings.add_webhook_desc": "Gitea 會發送含有指定 Content Type 的 POST 請求到目標 URL。 在 Webhook 指南閱讀更多內容。", "repo.settings.payload_url": "目標 URL", @@ -2162,9 +2126,7 @@ "repo.settings.block_outdated_branch": "如果合併請求已經過時則阻擋合併", "repo.settings.block_outdated_branch_desc": "當 head 分支落後於基礎分支時不得合併。", "repo.settings.block_admin_merge_override": "管理員必須遵守分支保護規則", - "repo.settings.default_branch_desc": "請選擇用來提交程式碼和合併請求的預設分支。", "repo.settings.merge_style_desc": "合併方式", - "repo.settings.default_merge_style_desc": "預設合併方式", "repo.settings.choose_branch": "選擇一個分支...", "repo.settings.no_protected_branch": "沒有受保護的分支。", "repo.settings.edit_protected_branch": "編輯", @@ -2283,6 +2245,7 @@ "repo.diff.review.reject": "請求變更", "repo.diff.review.self_approve": "合併請求的作者不能核可自己的合併請求", "repo.diff.committed_by": "提交者", + "repo.commits.avatar_stack_and": "和", "repo.diff.protected": "受保護", "repo.diff.image.side_by_side": "並列", "repo.diff.image.swipe": "滑動", @@ -2357,7 +2320,6 @@ "repo.branch.restore_success": "已還原分支「%s」。", "repo.branch.restore_failed": "還原分支「%s」失敗。", "repo.branch.protected_deletion_failed": "分支「%s」已被保護,不能刪除。", - "repo.branch.default_deletion_failed": "分支「%s」為預設分支,不能刪除。", "repo.branch.restore": "還原分支「%s」", "repo.branch.download": "下載分支「%s」", "repo.branch.rename": "重新命名分支「%s」", @@ -2441,9 +2403,7 @@ "org.settings.labels_desc": "在此處新增的標籤可用於此組織下的所有儲存庫。", "org.members.membership_visibility": "成員可見性:", "org.members.public": "可見", - "org.members.public_helper": "隱藏", "org.members.private": "隱藏", - "org.members.private_helper": "顯示", "org.members.member_role": "成員角色:", "org.members.owner": "擁有者", "org.members.member": "普通成員", @@ -2474,7 +2434,6 @@ "org.teams.members": "團隊成員", "org.teams.update_settings": "更新設定", "org.teams.delete_team": "刪除團隊", - "org.teams.add_team_member": "增加團隊成員", "org.teams.invite_team_member": "邀請至 %s", "org.teams.invite_team_member.list": "待處理的邀請", "org.teams.delete_team_title": "刪除團隊", @@ -2500,6 +2459,10 @@ "org.teams.all_repositories_read_permission_desc": "這個團隊擁有所有儲存庫讀取 權限:成員可以查看和 Clone 儲存庫。", "org.teams.all_repositories_write_permission_desc": "這個團隊擁有所有儲存庫寫入 權限:成員可以讀取和推送到儲存庫。", "org.teams.all_repositories_admin_permission_desc": "這個團隊擁有所有儲存庫管理員 權限:成員可以讀取、推送和增加協作者到儲存庫。", + "org.teams.visibility": "瀏覽權限", + "org.teams.visibility_private": "私有", + "org.teams.visibility_limited": "受限", + "org.teams.visibility_public": "公開", "org.teams.invite.title": "您已被邀請加入組織 %s 中的團隊 %s。", "org.teams.invite.by": "邀請人 %s", "org.teams.invite.description": "請點擊下方按鈕加入團隊。", @@ -2514,6 +2477,7 @@ "admin.hooks": "Webhook", "admin.integrations": "整合", "admin.authentication": "認證來源", + "admin.badges.description": "描述", "admin.config": "組態", "admin.config_summary": "摘要", "admin.config_settings": "設定", @@ -2808,11 +2772,8 @@ "admin.config.server_config": "伺服器組態", "admin.config.app_name": "網站標題", "admin.config.app_ver": "Gitea 版本", - "admin.config.app_url": "Gitea 基本 URL", "admin.config.custom_conf": "設定檔案路徑", "admin.config.custom_file_root_path": "自訂檔案根目錄", - "admin.config.domain": "伺服器域名", - "admin.config.offline_mode": "本地模式", "admin.config.disable_router_log": "關閉路由日誌", "admin.config.run_user": "以使用者名稱執行", "admin.config.run_mode": "執行模式", @@ -2862,7 +2823,6 @@ "admin.config.default_enable_timetracking": "預設啟用時間追蹤", "admin.config.default_allow_only_contributors_to_track_time": "只讓貢獻者追蹤時間", "admin.config.no_reply_address": "隱藏電子信箱域名", - "admin.config.default_visibility_organization": "新組織的預設瀏覽權限", "admin.config.default_enable_dependencies": "預設啟用問題的先決條件", "admin.config.webhook_config": "Webhook 組態", "admin.config.queue_length": "佇列長度", @@ -2889,7 +2849,6 @@ "admin.config.cache_config": "Cache 組態", "admin.config.cache_adapter": "Cache 適配器", "admin.config.cache_interval": "Cache 週期", - "admin.config.cache_conn": "Cache 連接字符串", "admin.config.cache_item_ttl": "快取項目 TTL", "admin.config.cache_test": "測試快取", "admin.config.cache_test_failed": "測試快取失敗: %v", @@ -2897,7 +2856,6 @@ "admin.config.cache_test_succeeded": "快取測試成功,回應時間為 %s", "admin.config.session_config": "Session 組態", "admin.config.session_provider": "Session 提供者", - "admin.config.provider_config": "提供者設定", "admin.config.cookie_name": "Cookie 名稱", "admin.config.gc_interval_time": "GC 週期", "admin.config.session_life_time": "Session 生命週期", @@ -2905,7 +2863,6 @@ "admin.config.cookie_life_time": "Cookie 生命週期", "admin.config.picture_config": "圖片和大頭貼組態", "admin.config.picture_service": "圖片服務", - "admin.config.disable_gravatar": "停用 Gravatar", "admin.config.enable_federated_avatar": "啟用 Federated Avatars", "admin.config.open_with_editor_app_help": "「開啟方式」編輯器用於克隆選單。如果留空,將使用預設值。展開以查看預設值。", "admin.config.git_config": "Git 組態", @@ -2916,8 +2873,6 @@ "admin.config.git_gc_args": "GC 參數", "admin.config.git_migrate_timeout": "遷移逾時", "admin.config.git_mirror_timeout": "鏡像更新超時", - "admin.config.git_clone_timeout": "Clone 作業逾時", - "admin.config.git_pull_timeout": "Pull 作業逾時", "admin.config.git_gc_timeout": "GC 作業逾時", "admin.config.log_config": "日誌組態", "admin.config.logger_name_fmt": "記錄器: %s", @@ -3150,6 +3105,10 @@ "packages.rubygems.required.rubygems": "需要的 RubyGem 版本", "packages.swift.install": "將此套件加入您的 Package.swift 檔:", "packages.swift.install2": "並執行下列命令:", + "packages.terraform.install2": "並執行下列命令:", + "packages.terraform.lock_status": "鎖定狀態", + "packages.terraform.lock": "鎖定", + "packages.terraform.unlock": "解除鎖定", "packages.vagrant.install": "執行下列命令以新增 Vagrant box:", "packages.settings.link": "連結此套件到儲存庫", "packages.settings.link.select": "選擇儲存庫", @@ -3256,6 +3215,8 @@ "actions.runs.status": "狀態", "actions.runs.actors_no_select": "所有執行者", "actions.runs.status_no_select": "所有狀態", + "actions.runs.branch": "分支", + "actions.runs.branches_no_select": "所有分支", "actions.runs.no_results": "沒有符合的結果。", "actions.runs.no_workflows": "目前還沒有工作流程。", "actions.runs.no_workflows.quick_start": "不知道如何開始使用 Gitea Actions?請參閱快速入門指南。", @@ -3263,11 +3224,16 @@ "actions.runs.no_runs": "工作流程沒有執行過。", "actions.runs.empty_commit_message": "(空的提交訊息)", "actions.runs.expire_log_message": "日誌已被清除,因為它們太舊了。", + "actions.runs.summary": "摘要", + "actions.runs.latest": "最新", "actions.workflow.disable": "停用工作流程", "actions.workflow.disable_success": "已成功停用工作流程「%s」。", "actions.workflow.enable": "啟用工作流程", "actions.workflow.enable_success": "已成功啟用工作流程「%s」。", "actions.workflow.disabled": "工作流程已停用。", + "actions.workflow.scope_owner": "擁有者", + "actions.workflow.scope_global": "全域", + "actions.workflow.required": "必要", "actions.workflow.run": "執行工作流程", "actions.workflow.not_found": "找不到工作流程「%s」。", "actions.workflow.run_success": "工作流程「%s」執行成功。", @@ -3294,8 +3260,6 @@ "projects.type-2.display_name": "儲存庫專案", "projects.type-3.display_name": "組織專案", "git.filemode.directory": "目錄", - "git.filemode.normal_file": "一般檔案", - "git.filemode.executable_file": "可執行檔", - "git.filemode.symbolic_link": "符號連結", - "git.filemode.submodule": "子模組" + "git.filemode.submodule": "子模組", + "actions.general.token_permissions.mode.restricted": "受限" } diff --git a/package.json b/package.json index 137d5a0783..2c19f0a382 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "type": "module", - "packageManager": "pnpm@11.5.3", + "packageManager": "pnpm@11.10.0", "engines": { "node": ">= 22.18.0", "pnpm": ">= 11.0.0" @@ -11,24 +11,24 @@ "@citation-js/plugin-csl": "0.7.22", "@citation-js/plugin-software-formats": "0.6.2", "@codemirror/autocomplete": "6.20.3", - "@codemirror/commands": "6.10.3", + "@codemirror/commands": "6.10.4", "@codemirror/lang-json": "6.0.2", "@codemirror/lang-markdown": "6.5.0", - "@codemirror/language": "6.12.3", + "@codemirror/language": "6.12.4", "@codemirror/language-data": "6.5.2", "@codemirror/legacy-modes": "6.5.3", "@codemirror/lint": "6.9.7", - "@codemirror/search": "6.7.0", - "@codemirror/state": "6.6.0", - "@codemirror/view": "6.43.1", + "@codemirror/search": "6.7.1", + "@codemirror/state": "6.7.1", + "@codemirror/view": "6.43.6", "@deltablot/dropzone": "7.4.3", "@github/markdown-toolbar-element": "2.2.3", "@github/paste-markdown": "1.5.3", "@github/text-expander-element": "2.9.4", "@lezer/highlight": "1.2.3", "@mcaptcha/vanilla-glue": "0.1.0-rc2", - "@mermaid-js/layout-elk": "0.2.1", - "@primer/octicons": "19.28.1", + "@mermaid-js/layout-elk": "0.2.2", + "@primer/octicons": "19.29.1", "@replit/codemirror-indentation-markers": "6.5.3", "@replit/codemirror-lang-nix": "6.0.1", "@replit/codemirror-lang-svelte": "6.0.0", @@ -36,11 +36,11 @@ "@resvg/resvg-wasm": "2.6.2", "@vitejs/plugin-vue": "6.0.7", "ansi_up": "6.0.6", - "asciinema-player": "3.15.1", + "asciinema-player": "3.17.0", "chart.js": "4.5.1", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", - "clippie": "4.2.0", + "clippie": "4.2.1", "codemirror-lang-elixir": "4.0.1", "colord": "2.9.3", "compare-versions": "6.1.1", @@ -50,16 +50,16 @@ "esbuild": "0.28.1", "idiomorph": "0.7.4", "jquery": "4.0.0", - "js-yaml": "4.2.0", + "js-yaml": "5.2.1", "katex": "0.17.0", - "mermaid": "11.15.0", + "mermaid": "11.16.0", "online-3d-viewer": "0.18.0", "pdfobject": "2.3.1", "perfect-debounce": "2.1.0", - "postcss": "8.5.15", + "postcss": "8.5.16", "rolldown-license-plugin": "3.0.9", "sortablejs": "1.15.7", - "swagger-ui-dist": "5.32.6", + "swagger-ui-dist": "5.32.8", "tailwindcss": "3.4.19", "throttle-debounce": "5.0.2", "tippy.js": "6.3.7", @@ -67,61 +67,56 @@ "tributejs": "5.1.3", "uint8-to-base64": "0.2.1", "vanilla-colorful": "0.7.2", - "vite": "8.0.16", - "vite-string-plugin": "2.0.4", - "vue": "3.5.37", - "vue-bar-graph": "2.2.0", - "vue-chartjs": "5.3.3" + "vite": "8.1.4", + "vite-string-plugin": "2.0.5", + "vue": "3.5.39", + "vue-chartjs": "5.3.4" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.7.2", - "@eslint/json": "2.0.0", - "@playwright/test": "1.60.0", + "@eslint/json": "2.0.1", + "@playwright/test": "1.61.1", "@stylistic/eslint-plugin": "5.10.0", - "@stylistic/stylelint-plugin": "5.2.0", + "@stylistic/stylelint-plugin": "5.2.1", "@types/codemirror": "5.60.17", "@types/jquery": "4.0.1", - "@types/js-yaml": "4.0.9", "@types/katex": "0.16.8", - "@types/node": "25.9.3", + "@types/node": "26.1.1", "@types/pdfobject": "2.2.5", "@types/sortablejs": "1.15.9", "@types/swagger-ui-dist": "3.30.6", "@types/throttle-debounce": "5.0.2", "@types/toastify-js": "1.12.4", - "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/parser": "8.63.0", "@vitejs/plugin-vue": "6.0.7", - "@vitest/eslint-plugin": "1.6.20", - "eslint": "10.4.1", + "@vitest/eslint-plugin": "1.6.22", + "eslint": "10.6.0", "eslint-import-resolver-typescript": "4.4.5", - "eslint-plugin-array-func": "5.1.1", - "eslint-plugin-de-morgan": "2.1.2", - "eslint-plugin-github": "6.0.0", - "eslint-plugin-import-x": "4.16.2", - "eslint-plugin-playwright": "2.10.4", - "eslint-plugin-regexp": "3.1.0", - "eslint-plugin-sonarjs": "4.0.3", - "eslint-plugin-unicorn": "64.0.0", + "eslint-plugin-import-x": "4.17.1", + "eslint-plugin-playwright": "2.10.5", + "eslint-plugin-regexp": "3.1.1", + "eslint-plugin-sonarjs": "4.1.0", + "eslint-plugin-unicorn": "71.1.0", "eslint-plugin-vue": "10.9.2", "eslint-plugin-vue-scoped-css": "3.1.1", "eslint-plugin-wc": "3.1.0", - "globals": "17.6.0", - "happy-dom": "20.10.2", + "globals": "17.7.0", + "happy-dom": "20.10.6", "jiti": "2.7.0", - "markdownlint-cli": "0.48.0", - "material-icon-theme": "5.35.0", + "markdownlint-cli": "0.49.0", + "material-icon-theme": "5.36.1", "postcss-html": "1.8.1", - "spectral-cli-bundle": "1.0.8", - "stylelint": "17.13.0", + "spectral-cli-bundle": "1.0.11", + "stylelint": "17.14.0", "stylelint-config-recommended": "18.0.0", "stylelint-declaration-block-no-ignored-properties": "3.0.0", "stylelint-declaration-strict-value": "1.11.1", "stylelint-value-no-unknown-custom-properties": "6.1.1", "svgo": "4.0.1", "typescript": "6.0.3", - "typescript-eslint": "8.61.0", - "updates": "17.18.0", - "vitest": "4.1.8", - "vue-tsc": "3.3.4" + "typescript-eslint": "8.63.0", + "updates": "17.18.2", + "vitest": "4.1.10", + "vue-tsc": "3.3.7" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90b34abce4..7e7641875f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: 6.20.3 version: 6.20.3 '@codemirror/commands': - specifier: 6.10.3 - version: 6.10.3 + specifier: 6.10.4 + version: 6.10.4 '@codemirror/lang-json': specifier: 6.0.2 version: 6.0.2 @@ -33,8 +33,8 @@ importers: specifier: 6.5.0 version: 6.5.0 '@codemirror/language': - specifier: 6.12.3 - version: 6.12.3 + specifier: 6.12.4 + version: 6.12.4 '@codemirror/language-data': specifier: 6.5.2 version: 6.5.2 @@ -45,14 +45,14 @@ importers: specifier: 6.9.7 version: 6.9.7 '@codemirror/search': - specifier: 6.7.0 - version: 6.7.0 + specifier: 6.7.1 + version: 6.7.1 '@codemirror/state': - specifier: 6.6.0 - version: 6.6.0 + specifier: 6.7.1 + version: 6.7.1 '@codemirror/view': - specifier: 6.43.1 - version: 6.43.1 + specifier: 6.43.6 + version: 6.43.6 '@deltablot/dropzone': specifier: 7.4.3 version: 7.4.3 @@ -72,35 +72,35 @@ importers: specifier: 0.1.0-rc2 version: 0.1.0-rc2 '@mermaid-js/layout-elk': - specifier: 0.2.1 - version: 0.2.1(mermaid@11.15.0) + specifier: 0.2.2 + version: 0.2.2(mermaid@11.16.0) '@primer/octicons': - specifier: 19.28.1 - version: 19.28.1 + specifier: 19.29.1 + version: 19.29.1 '@replit/codemirror-indentation-markers': specifier: 6.5.3 - version: 6.5.3(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.1) + version: 6.5.3(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.6) '@replit/codemirror-lang-nix': specifier: 6.0.1 - version: 6.0.1(@codemirror/autocomplete@6.20.3)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.1)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.10) + version: 6.0.1(@codemirror/autocomplete@6.20.3)(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.6)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.10) '@replit/codemirror-lang-svelte': specifier: 6.0.0 - version: 6.0.0(@codemirror/autocomplete@6.20.3)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.1)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.10) + version: 6.0.0(@codemirror/autocomplete@6.20.3)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.6)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.10) '@replit/codemirror-vscode-keymap': specifier: 6.0.2 - version: 6.0.2(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.43.1) + version: 6.0.2(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.1)(@codemirror/view@6.43.6) '@resvg/resvg-wasm': specifier: 2.6.2 version: 2.6.2 '@vitejs/plugin-vue': specifier: 6.0.7 - version: 6.0.7(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0))(vue@3.5.37(typescript@6.0.3)) + version: 6.0.7(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39(typescript@6.0.3)) ansi_up: specifier: 6.0.6 version: 6.0.6 asciinema-player: - specifier: 3.15.1 - version: 3.15.1 + specifier: 3.17.0 + version: 3.17.0 chart.js: specifier: 4.5.1 version: 4.5.1 @@ -111,8 +111,8 @@ importers: specifier: 2.2.0 version: 2.2.0(chart.js@4.5.1) clippie: - specifier: 4.2.0 - version: 4.2.0 + specifier: 4.2.1 + version: 4.2.1 codemirror-lang-elixir: specifier: 4.0.1 version: 4.0.1 @@ -141,14 +141,14 @@ importers: specifier: 4.0.0 version: 4.0.0 js-yaml: - specifier: 4.2.0 - version: 4.2.0 + specifier: 5.2.1 + version: 5.2.1 katex: specifier: 0.17.0 version: 0.17.0 mermaid: - specifier: 11.15.0 - version: 11.15.0 + specifier: 11.16.0 + version: 11.16.0 online-3d-viewer: specifier: 0.18.0 version: 0.18.0 @@ -159,20 +159,20 @@ importers: specifier: 2.1.0 version: 2.1.0 postcss: - specifier: 8.5.15 - version: 8.5.15 + specifier: 8.5.16 + version: 8.5.16 rolldown-license-plugin: specifier: 3.0.9 - version: 3.0.9(rolldown@1.0.3) + version: 3.0.9(rolldown@1.1.5) sortablejs: specifier: 1.15.7 version: 1.15.7 swagger-ui-dist: - specifier: 5.32.6 - version: 5.32.6 + specifier: 5.32.8 + version: 5.32.8 tailwindcss: specifier: 3.4.19 - version: 3.4.19 + version: 3.4.19(yaml@2.9.0) throttle-debounce: specifier: 5.0.2 version: 5.0.2 @@ -192,51 +192,45 @@ importers: specifier: 0.7.2 version: 0.7.2 vite: - specifier: 8.0.16 - version: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0) + specifier: 8.1.4 + version: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) vite-string-plugin: - specifier: 2.0.4 - version: 2.0.4(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)) + specifier: 2.0.5 + version: 2.0.5(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) vue: - specifier: 3.5.37 - version: 3.5.37(typescript@6.0.3) - vue-bar-graph: - specifier: 2.2.0 - version: 2.2.0(typescript@6.0.3) + specifier: 3.5.39 + version: 3.5.39(typescript@6.0.3) vue-chartjs: - specifier: 5.3.3 - version: 5.3.3(chart.js@4.5.1)(vue@3.5.37(typescript@6.0.3)) + specifier: 5.3.4 + version: 5.3.4(chart.js@4.5.1)(vue@3.5.39(typescript@6.0.3)) devDependencies: '@eslint-community/eslint-plugin-eslint-comments': specifier: 4.7.2 - version: 4.7.2(eslint@10.4.1(jiti@2.7.0)) + version: 4.7.2(eslint@10.6.0(jiti@2.7.0)) '@eslint/json': - specifier: 2.0.0 - version: 2.0.0 + specifier: 2.0.1 + version: 2.0.1 '@playwright/test': - specifier: 1.60.0 - version: 1.60.0 + specifier: 1.61.1 + version: 1.61.1 '@stylistic/eslint-plugin': specifier: 5.10.0 - version: 5.10.0(eslint@10.4.1(jiti@2.7.0)) + version: 5.10.0(eslint@10.6.0(jiti@2.7.0)) '@stylistic/stylelint-plugin': - specifier: 5.2.0 - version: 5.2.0(stylelint@17.13.0(typescript@6.0.3)) + specifier: 5.2.1 + version: 5.2.1(stylelint@17.14.0(typescript@6.0.3)) '@types/codemirror': specifier: 5.60.17 version: 5.60.17 '@types/jquery': specifier: 4.0.1 version: 4.0.1 - '@types/js-yaml': - specifier: 4.0.9 - version: 4.0.9 '@types/katex': specifier: 0.16.8 version: 0.16.8 '@types/node': - specifier: 25.9.3 - version: 25.9.3 + specifier: 26.1.1 + version: 26.1.1 '@types/pdfobject': specifier: 2.2.5 version: 2.2.5 @@ -253,86 +247,77 @@ importers: specifier: 1.12.4 version: 1.12.4 '@typescript-eslint/parser': - specifier: 8.61.0 - version: 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + specifier: 8.63.0 + version: 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@vitest/eslint-plugin': - specifier: 1.6.20 - version: 1.6.20(@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8(@types/node@25.9.3)(happy-dom@20.10.2)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0))) + specifier: 1.6.22 + version: 1.6.22(@typescript-eslint/eslint-plugin@8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.10(@types/node@26.1.1)(happy-dom@20.10.6)(jsdom@20.0.3)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))) eslint: - specifier: 10.4.1 - version: 10.4.1(jiti@2.7.0) + specifier: 10.6.0 + version: 10.6.0(jiti@2.7.0) eslint-import-resolver-typescript: specifier: 4.4.5 - version: 4.4.5(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.1(jiti@2.7.0)) - eslint-plugin-array-func: - specifier: 5.1.1 - version: 5.1.1(eslint@10.4.1(jiti@2.7.0)) - eslint-plugin-de-morgan: - specifier: 2.1.2 - version: 2.1.2(eslint@10.4.1(jiti@2.7.0)) - eslint-plugin-github: - specifier: 6.0.0 - version: 6.0.0(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)) + version: 4.4.5(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.6.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.6.0(jiti@2.7.0)) eslint-plugin-import-x: - specifier: 4.16.2 - version: 4.16.2(@typescript-eslint/utils@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)) + specifier: 4.17.1 + version: 4.17.1(@typescript-eslint/utils@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.6.0(jiti@2.7.0)) eslint-plugin-playwright: - specifier: 2.10.4 - version: 2.10.4(eslint@10.4.1(jiti@2.7.0)) + specifier: 2.10.5 + version: 2.10.5(eslint@10.6.0(jiti@2.7.0)) eslint-plugin-regexp: - specifier: 3.1.0 - version: 3.1.0(eslint@10.4.1(jiti@2.7.0)) + specifier: 3.1.1 + version: 3.1.1(eslint@10.6.0(jiti@2.7.0)) eslint-plugin-sonarjs: - specifier: 4.0.3 - version: 4.0.3(eslint@10.4.1(jiti@2.7.0)) + specifier: 4.1.0 + version: 4.1.0(eslint@10.6.0(jiti@2.7.0)) eslint-plugin-unicorn: - specifier: 64.0.0 - version: 64.0.0(eslint@10.4.1(jiti@2.7.0)) + specifier: 71.1.0 + version: 71.1.0(eslint@10.6.0(jiti@2.7.0)) eslint-plugin-vue: specifier: 10.9.2 - version: 10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@10.4.1(jiti@2.7.0)))(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.1(jiti@2.7.0))) + version: 10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@10.6.0(jiti@2.7.0)))(@typescript-eslint/parser@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.6.0(jiti@2.7.0))) eslint-plugin-vue-scoped-css: specifier: 3.1.1 - version: 3.1.1(eslint@10.4.1(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.1(jiti@2.7.0))) + version: 3.1.1(eslint@10.6.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.6.0(jiti@2.7.0))) eslint-plugin-wc: specifier: 3.1.0 - version: 3.1.0(eslint@10.4.1(jiti@2.7.0)) + version: 3.1.0(eslint@10.6.0(jiti@2.7.0)) globals: - specifier: 17.6.0 - version: 17.6.0 + specifier: 17.7.0 + version: 17.7.0 happy-dom: - specifier: 20.10.2 - version: 20.10.2 + specifier: 20.10.6 + version: 20.10.6 jiti: specifier: 2.7.0 version: 2.7.0 markdownlint-cli: - specifier: 0.48.0 - version: 0.48.0 + specifier: 0.49.0 + version: 0.49.0 material-icon-theme: - specifier: 5.35.0 - version: 5.35.0 + specifier: 5.36.1 + version: 5.36.1 postcss-html: specifier: 1.8.1 version: 1.8.1 spectral-cli-bundle: - specifier: 1.0.8 - version: 1.0.8 + specifier: 1.0.11 + version: 1.0.11 stylelint: - specifier: 17.13.0 - version: 17.13.0(typescript@6.0.3) + specifier: 17.14.0 + version: 17.14.0(typescript@6.0.3) stylelint-config-recommended: specifier: 18.0.0 - version: 18.0.0(stylelint@17.13.0(typescript@6.0.3)) + version: 18.0.0(stylelint@17.14.0(typescript@6.0.3)) stylelint-declaration-block-no-ignored-properties: specifier: 3.0.0 - version: 3.0.0(stylelint@17.13.0(typescript@6.0.3)) + version: 3.0.0(stylelint@17.14.0(typescript@6.0.3)) stylelint-declaration-strict-value: specifier: 1.11.1 - version: 1.11.1(stylelint@17.13.0(typescript@6.0.3)) + version: 1.11.1(stylelint@17.14.0(typescript@6.0.3)) stylelint-value-no-unknown-custom-properties: specifier: 6.1.1 - version: 6.1.1(stylelint@17.13.0(typescript@6.0.3)) + version: 6.1.1(stylelint@17.14.0(typescript@6.0.3)) svgo: specifier: 4.0.1 version: 4.0.1 @@ -340,17 +325,17 @@ importers: specifier: 6.0.3 version: 6.0.3 typescript-eslint: - specifier: 8.61.0 - version: 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + specifier: 8.63.0 + version: 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) updates: - specifier: 17.18.0 - version: 17.18.0 + specifier: 17.18.2 + version: 17.18.2 vitest: - specifier: 4.1.8 - version: 4.1.8(@types/node@25.9.3)(happy-dom@20.10.2)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)) + specifier: 4.1.10 + version: 4.1.10(@types/node@26.1.1)(happy-dom@20.10.6)(jsdom@20.0.3)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) vue-tsc: - specifier: 3.3.4 - version: 3.3.4(typescript@6.0.3) + specifier: 3.3.7 + version: 3.3.7(typescript@6.0.3) packages: @@ -449,8 +434,8 @@ packages: '@codemirror/autocomplete@6.20.3': resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} - '@codemirror/commands@6.10.3': - resolution: {integrity: sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==} + '@codemirror/commands@6.10.4': + resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==} '@codemirror/lang-angular@0.1.4': resolution: {integrity: sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==} @@ -518,8 +503,8 @@ packages: '@codemirror/language-data@6.5.2': resolution: {integrity: sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==} - '@codemirror/language@6.12.3': - resolution: {integrity: sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==} + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} '@codemirror/legacy-modes@6.5.3': resolution: {integrity: sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==} @@ -527,14 +512,14 @@ packages: '@codemirror/lint@6.9.7': resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} - '@codemirror/search@6.7.0': - resolution: {integrity: sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==} + '@codemirror/search@6.7.1': + resolution: {integrity: sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==} - '@codemirror/state@6.6.0': - resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==} + '@codemirror/state@6.7.1': + resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==} - '@codemirror/view@6.43.1': - resolution: {integrity: sha512-+BIjw/AG3tDQ4pJgTLPYdAW25eDE66YsvM4LKyVPgGzVgZ4a9Wj1SRX8kPVKgBDdPt8oHtZ15F0qx7p0oOHdHw==} + '@codemirror/view@6.43.6': + resolution: {integrity: sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==} '@csstools/css-calc@3.2.1': resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} @@ -549,8 +534,8 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.4': - resolution: {integrity: sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==} + '@csstools/css-syntax-patches-for-csstree@1.1.6': + resolution: {integrity: sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==} peerDependencies: css-tree: ^3.2.1 peerDependenciesMeta: @@ -586,12 +571,21 @@ packages: '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} @@ -764,15 +758,6 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/compat@1.4.1': - resolution: {integrity: sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.40 || 9 - peerDependenciesMeta: - eslint: - optional: true - '@eslint/config-array@0.23.5': resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -781,24 +766,12 @@ packages: resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@1.2.1': resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/eslintrc@3.3.5': - resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.39.4': - resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/json@2.0.0': - resolution: {integrity: sha512-P32ZJMIopNWQd1SFhd0tgjfA/hgzUuVSqHmMi2273QaLWHWimXq6V+qL4DNKnjGzO/aNECtYW+rEJ/pWB6uP+w==} + '@eslint/json@2.0.1': + resolution: {integrity: sha512-Thz2j92ceUF3Bq/0TuWb3MWn3Z+Cwc8k5ptF0fakl2D4Mp8mSx07Xr1aQM4R5NoihzarWUdxfOmQ8DesGy4jOg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/object-schema@3.0.5': @@ -809,9 +782,6 @@ packages: resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@github/browserslist-config@1.0.0': - resolution: {integrity: sha512-gIhjdJp/c2beaIWWIlsXdqXVRUz3r2BxBCpfz/F3JXHvSAQ1paMYjLH+maEATtENg+k5eLV7gA+9yPp762ieuw==} - '@github/combobox-nav@2.3.1': resolution: {integrity: sha512-gwxPzLw8XKecy1nP63i9lOBritS3bWmxl02UX6G0TwMQZbMem1BCS1tEZgYd3mkrkiDrUMWaX+DbFCuDFo3K+A==} @@ -955,16 +925,16 @@ packages: '@mcaptcha/vanilla-glue@0.1.0-rc2': resolution: {integrity: sha512-LDjn9lrKioJ3zwaQOfql7PXsnxCAHg7b1rPw7G0OxpvVE7xLB/a40SHfIIiocce2VS9TPI4MbcKm5pcuy8fU5g==} - '@mermaid-js/layout-elk@0.2.1': - resolution: {integrity: sha512-MX9jwhMyd5zDcFsYcl3duDUkKhjVRUCGEQrdCeNV5hCIR6+3FuDDbRbFmvVbAu15K1+juzsYGG+K8MDvCY1Amg==} + '@mermaid-js/layout-elk@0.2.2': + resolution: {integrity: sha512-vnH3gtqfhyBiRVKNpT8iDENTw18q/OF0GF/SfYfHN43KZpu+6eZDEOMHTfNYAkpmUWJNgtRQFIS6BTc7vH/DYQ==} peerDependencies: mermaid: ^11.0.2 - '@mermaid-js/parser@1.1.1': - resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} + '@mermaid-js/parser@1.2.0': + resolution: {integrity: sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==} - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 @@ -981,26 +951,19 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} - '@package-json/types@0.0.12': - resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} - - '@pkgr/core@0.3.6': - resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} - engines: {node: ^14.18.0 || >=16.0.0} - - '@playwright/test@1.60.0': - resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} engines: {node: '>=18'} hasBin: true '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@primer/octicons@19.28.1': - resolution: {integrity: sha512-pwSilXmgNrbVF2bChkh4zZtUyb4Vr4niYhA9PhUdtjVz86A2iwA/YjjopHS0suT+I7niUZJEepEpmSC7kARKNQ==} + '@primer/octicons@19.29.1': + resolution: {integrity: sha512-XVRArUP8bN+IEjfNQSquXAJinVqAWRx36sVvhN5VzC+gOLfbx16EfFFvcFhSVaqQJ+57PjBrbj75oqm8VlOoYQ==} '@replit/codemirror-indentation-markers@6.5.3': resolution: {integrity: sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw==} @@ -1050,97 +1013,97 @@ packages: resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} engines: {node: '>= 10'} - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1194,8 +1157,8 @@ packages: peerDependencies: eslint: ^9.0.0 || ^10.0.0 - '@stylistic/stylelint-plugin@5.2.0': - resolution: {integrity: sha512-pfa2PMvlH87qwoOMUH4rqz7x/vuO5Kh+r/R1Pe+/5T8sukDPe/VfF6mbdUGE5gwZPlj5O1X56qw55IZrFFRF0g==} + '@stylistic/stylelint-plugin@5.2.1': + resolution: {integrity: sha512-BMRWJVtO16uADJmKcGi6mreei/QYdOviABXuaCeHEc3T3pxo4wFYYSvnxIoyiPVfDJqX+DxQdnGkuc6KxsRaSw==} peerDependencies: stylelint: ^17.6.0 @@ -1206,8 +1169,8 @@ packages: resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==} engines: {node: '>= 10'} - '@tybys/wasm-util@0.10.2': - resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1338,9 +1301,6 @@ packages: '@types/jquery@4.0.1': resolution: {integrity: sha512-9a59A/tycXgYuPABcp6/3spSShn0NT2UOM4EfHvMumjYi4lJWTsK5SZWjhx3yRm9IHGCeWXdV2YfNsrWrft/CA==} - '@types/js-yaml@4.0.9': - resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} - '@types/jsdom@20.0.1': resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} @@ -1359,8 +1319,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@25.9.3': - resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==} + '@types/node@26.1.1': + resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} '@types/pdfobject@2.2.5': resolution: {integrity: sha512-7gD5tqc/RUDq0PyoLemL0vEHxBYi+zY0WVaFAx/Y0jBsXFgot1vB9No1GhDZGwRGJMCIZbgAb74QG9MTyTNU/g==} @@ -1404,63 +1364,63 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.61.0': - resolution: {integrity: sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==} + '@typescript-eslint/eslint-plugin@8.63.0': + resolution: {integrity: sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.61.0 + '@typescript-eslint/parser': ^8.63.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.61.0': - resolution: {integrity: sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==} + '@typescript-eslint/parser@8.63.0': + resolution: {integrity: sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.61.0': - resolution: {integrity: sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==} + '@typescript-eslint/project-service@8.63.0': + resolution: {integrity: sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.61.0': - resolution: {integrity: sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==} + '@typescript-eslint/scope-manager@8.63.0': + resolution: {integrity: sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.61.0': - resolution: {integrity: sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==} + '@typescript-eslint/tsconfig-utils@8.63.0': + resolution: {integrity: sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.61.0': - resolution: {integrity: sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==} + '@typescript-eslint/type-utils@8.63.0': + resolution: {integrity: sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.61.0': - resolution: {integrity: sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==} + '@typescript-eslint/types@8.63.0': + resolution: {integrity: sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.61.0': - resolution: {integrity: sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==} + '@typescript-eslint/typescript-estree@8.63.0': + resolution: {integrity: sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.61.0': - resolution: {integrity: sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==} + '@typescript-eslint/utils@8.63.0': + resolution: {integrity: sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.61.0': - resolution: {integrity: sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==} + '@typescript-eslint/visitor-keys@8.63.0': + resolution: {integrity: sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@unrs/resolver-binding-android-arm-eabi@1.12.2': @@ -1593,8 +1553,8 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.2.25 - '@vitest/eslint-plugin@1.6.20': - resolution: {integrity: sha512-xRwWHFG0Utp6hXtbGiWk4VdKXCGdExD8kbWrrmFEiG5dk8anOJ+vbWbeOa8EbkocKQRTsx7JAWETccZiBgFp/Q==} + '@vitest/eslint-plugin@1.6.22': + resolution: {integrity: sha512-SDHPcido/3V5NdQh1rmOIMTHSixtjUBdV3a/8aE/A3Iwx/EZtVuLRC+e0g1uenh4Yd1fjps0JIN5HUNmhdIpQQ==} engines: {node: '>=18'} peerDependencies: '@typescript-eslint/eslint-plugin': '*' @@ -1609,11 +1569,11 @@ packages: vitest: optional: true - '@vitest/expect@4.1.8': - resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - '@vitest/mocker@4.1.8': - resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1623,20 +1583,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.8': - resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - '@vitest/runner@4.1.8': - resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - '@vitest/snapshot@4.1.8': - resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - '@vitest/spy@4.1.8': - resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - '@vitest/utils@4.1.8': - resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} '@volar/language-core@2.4.28': resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} @@ -1647,37 +1607,37 @@ packages: '@volar/typescript@2.4.28': resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} - '@vue/compiler-core@3.5.37': - resolution: {integrity: sha512-TfQz4bsBQTPoTeBWTUPJPq+4FCTTXg2pbp8TjjAyrGaLAu9nfrZTxKLf6mdAlclnwtyUToFaMQu7QRS63Qek1g==} + '@vue/compiler-core@3.5.39': + resolution: {integrity: sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==} - '@vue/compiler-dom@3.5.37': - resolution: {integrity: sha512-oqfl/QaCVEWxphALFEZ7m+q9z+Sghz9ZCcoJ/oplTGxsOgx2czVzSZxkMkzQrWIahywOeyGHdg9ml/WUz3DMzw==} + '@vue/compiler-dom@3.5.39': + resolution: {integrity: sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==} - '@vue/compiler-sfc@3.5.37': - resolution: {integrity: sha512-hYu+efs678xaPHYxhFRK3ZhkQ/FueMVnROooZqemOYlyQBQg06qkIrpyAUrUWWqMLfifgOdWwU6CL6FuTRvP4A==} + '@vue/compiler-sfc@3.5.39': + resolution: {integrity: sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==} - '@vue/compiler-ssr@3.5.37': - resolution: {integrity: sha512-ihbdCLJLXFKV3efEQlFfzy6TLHRuOQ/+dze2vZfg0DIncVxkcUxwCqPewCiSVdWXFeoiuMMON87wpt+G+yT22A==} + '@vue/compiler-ssr@3.5.39': + resolution: {integrity: sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==} - '@vue/language-core@3.3.4': - resolution: {integrity: sha512-IuHqQ5zGGOE7CXP72VX6A42IVeIzYv4WAhO6arej11TRNqtdZfGyH8Yr2FOCaDX0dSQG+JwULLoFHGY1igYVjQ==} + '@vue/language-core@3.3.7': + resolution: {integrity: sha512-LzmkKinXAMMoh8Jfi/jMUSDUjuPdv8mynH5WJGKfXyZtDw3hQ6GBaoI6Bcnl/Xqlu32q/0Z6i/trp4VXykzyLw==} - '@vue/reactivity@3.5.37': - resolution: {integrity: sha512-M7j7YF68IUd2uFNIqhwybpzUG/Sk9HUtk+ULmC+g6JeZ80LyCyGnjv6SYBR86t3fyyuYlZUSb18yu4UYLgw5jg==} + '@vue/reactivity@3.5.39': + resolution: {integrity: sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==} - '@vue/runtime-core@3.5.37': - resolution: {integrity: sha512-N0IWRirNPzJp/DuUCR9M+obVUHZArMkmldRApKsJRIWA+XDO6iwF4Zh94HP6uCzYVgWVwr8YuKeWF4H52VxTbw==} + '@vue/runtime-core@3.5.39': + resolution: {integrity: sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==} - '@vue/runtime-dom@3.5.37': - resolution: {integrity: sha512-9VkutCFwfVOiMRH7mgi7QapsqC8Hxcow3DLvBKf+mRH88P94Ib/D/u6l/ln62ST+fIvmsOO7+Db99LzWv9slJg==} + '@vue/runtime-dom@3.5.39': + resolution: {integrity: sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==} - '@vue/server-renderer@3.5.37': - resolution: {integrity: sha512-CP7nbxJb1Zc0/oeBqu6CtMf9TN64fz6qE75BZ8mWh04zAEya8EAZmqH2gRQWkoUUjFqv9i7h/mV+E4/LL4JXXw==} + '@vue/server-renderer@3.5.39': + resolution: {integrity: sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==} peerDependencies: - vue: 3.5.37 + vue: 3.5.39 - '@vue/shared@3.5.37': - resolution: {integrity: sha512-JzFx4aYGz+EtBl8zzw8XECNWSmNXTrU5jpub3T1lQ+2X5Ys9QzHWafxhE+E5qS2Ry/mVl8o2QqrwRGYYOFYguw==} + '@vue/shared@3.5.39': + resolution: {integrity: sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==} abab@2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} @@ -1713,14 +1673,6 @@ packages: alien-signals@3.2.1: resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} - ansi-escapes@1.4.0: - resolution: {integrity: sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw==} - engines: {node: '>=0.10.0'} - - ansi-regex@2.1.1: - resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} - engines: {node: '>=0.10.0'} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1729,10 +1681,6 @@ packages: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} - ansi-styles@2.2.1: - resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} - engines: {node: '>=0.10.0'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1757,10 +1705,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} - engines: {node: '>= 0.4'} - array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -1785,23 +1729,13 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} - asciinema-player@3.15.1: - resolution: {integrity: sha512-agVYeNlPxthLyAb92l9AS7ypW0uhesqOuQzyR58Q4Sj+MvesQztZBgx86lHqNJkB8rQ6EP0LeA9czGytQUBpYw==} - - asn1@0.2.6: - resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} - - assert-plus@1.0.0: - resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} - engines: {node: '>=0.8'} + asciinema-player@3.17.0: + resolution: {integrity: sha512-JbjNJmA2TLIeYNaOEja+kVSzXadKoqpIzVVmfBGNj2DmdtE/vExBCnkE8NYEcpaQcDvUYg8Ltt0urT80frACmw==} assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-types-flow@0.0.8: - resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} - astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -1817,20 +1751,6 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - aws-sign2@0.7.0: - resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} - - aws4@1.13.2: - resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} - - axe-core@4.12.0: - resolution: {integrity: sha512-FTavr/7Ba0IptwGOPxnQvdyW2tAsdLBMTBXz7rKH6xJ2skpyxpBxyHkDdBs4lf69yRqYpkqCdfhnwS8YULGOmg==} - engines: {node: '>=4'} - - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1846,20 +1766,15 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - bcrypt-pbkdf@1.0.2: - resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + baseline-browser-mapping@2.10.43: + resolution: {integrity: sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==} + engines: {node: '>=6.0.0'} + hasBin: true binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - biome@0.3.3: - resolution: {integrity: sha512-4LXjrQYbn9iTXu9Y4SKT7ABzTV0WnLDHCVSd2fPUOKsy1gQ+E4xPFmlY1zcWexoi0j7fGHItlL6OWA2CZ/yYAQ==} - hasBin: true - - bluebird@3.7.2: - resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -1879,6 +1794,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.6: + resolution: {integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-image-size@0.6.4: resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} engines: {node: '>=4.0'} @@ -1924,17 +1844,13 @@ packages: caniuse-lite@1.0.30001793: resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} - caseless@0.12.0: - resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + caniuse-lite@1.0.30001805: + resolution: {integrity: sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==} chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} - chalk@1.1.3: - resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} - engines: {node: '>=0.10.0'} - chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1985,23 +1901,8 @@ packages: citeproc@2.4.63: resolution: {integrity: sha512-68F95Bp4UbgZU/DBUGQn0qV3HDZLCdI9+Bb2ByrTaNJDL5VEm9LqaiNaxljsvoaExSLEXe1/r6n2Z06SCzW3/Q==} - clean-regexp@1.0.0: - resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} - engines: {node: '>=4'} - - cli-cursor@1.0.2: - resolution: {integrity: sha512-25tABq090YNKkF6JH7lcwO0zFJTRke4Jcq9iX2nr/Sz0Cjjv4gckmwlW6Ty/aoyFd6z3ysR2hMGC2GFugmBo6A==} - engines: {node: '>=0.10.0'} - - cli-width@1.1.1: - resolution: {integrity: sha512-eMU2akIeEIkCxGXUNmDnJq1KzOIiPnJ+rKqRe6hcxE3vIOPvpMrBYOn/Bl7zNlYJj/zQxXquAnozHUCf9Whnsg==} - - clippie@4.2.0: - resolution: {integrity: sha512-NcWaVzqChJ69+foFhwJXta3KXWNDJlpicxcfZG5udobyszOSBDhmFubKv1b/1nIZiVAsPoKqME2iV1SITZqFoQ==} - - code-point-at@1.1.0: - resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==} - engines: {node: '>=0.10.0'} + clippie@4.2.1: + resolution: {integrity: sha512-+Shk4WLaaBtxTwgaspGnFm/WrtjZCVCKsl66mPglNRj6B+OzgIYnp4LX7EjW1UTG5NfJsW60jxwV8EvqtyrDUA==} codemirror-lang-elixir@4.0.1: resolution: {integrity: sha512-z6W/XB4b7TZrp9EZYBGVq93vQfvKbff+1iM8YZaVErL0dguBAeLmVRlEv1NuDZHOP1qjJ3NwyibkUkNWn7q9VQ==} @@ -2030,12 +1931,9 @@ packages: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} - commander@14.0.3: - resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} - engines: {node: '>=20'} - - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} @@ -2059,30 +1957,27 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + convert-hrtime@5.0.0: + resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==} + engines: {node: '>=12'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} core-js-compat@3.49.0: resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} - core-js@2.6.12: - resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} - deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. - core-js@3.32.2: resolution: {integrity: sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==} - core-util-is@1.0.2: - resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} - cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} cose-base@2.2.0: resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} - cosmiconfig@9.0.1: - resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} + cosmiconfig@9.0.2: + resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==} engines: {node: '>=14'} peerDependencies: typescript: '>=4.9.5' @@ -2297,13 +2192,6 @@ packages: dagre-d3-es@7.0.14: resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} - damerau-levenshtein@1.0.8: - resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - - dashdash@1.14.1: - resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} - engines: {node: '>=0.10'} - data-urls@3.0.2: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} engines: {node: '>=12'} @@ -2353,10 +2241,6 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deep-rename-keys@0.2.1: - resolution: {integrity: sha512-RHd9ABw4Fvk+gYDWqwOftG849x0bYOySl/RgX0tLI9i27ZIeSO91mLZJEp7oPHOMFqHvpgu21YptmDt0FYD/0A==} - engines: {node: '>=0.10.0'} - define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -2376,6 +2260,10 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-indent@7.0.2: + resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==} + engines: {node: '>=12.20'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -2421,30 +2309,21 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - earlgrey-runtime@0.1.2: - resolution: {integrity: sha512-T4qoScXi5TwALDv8nlGTvOuCT8jXcKcxtO8qVdqv46IA2GHJfQzwoBPbkOmORnyhu3A98cVVuhWLsM2CzPljJg==} - easymde@2.21.0: resolution: {integrity: sha512-5uE7I/DEN8gvGRwxaqAv7h1PMEK2ykNXVX5zL0dK3nCYROGja3AMbdQz8eCEELnfvCfy7tRkTmLuvyJG8uSWjQ==} - ecc-jsbn@0.1.2: - resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} - - editor@1.0.0: - resolution: {integrity: sha512-SoRmbGStwNYHgKfjOrX2L0mUvp9bUVv0uPppZSOMAntEbcFtoC3MKF5b3T6HQPXKIV+QGY3xPO3JK5it5lVkuw==} - electron-to-chromium@1.5.364: resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} + electron-to-chromium@1.5.389: + resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==} + elkjs@0.9.3: resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -2507,10 +2386,6 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} @@ -2524,12 +2399,6 @@ packages: engines: {node: '>=6.0'} hasBin: true - eslint-config-prettier@10.1.8: - resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - eslint-import-context@0.1.9: resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2576,47 +2445,8 @@ packages: eslint-import-resolver-webpack: optional: true - eslint-plugin-array-func@5.1.1: - resolution: {integrity: sha512-TbVGk+yLqXHgtrS4DnYzg2Ycuk5y+lYFy5NgT748neQdJvNIYUucxp2QQjPU7dwbs9xp9fyktgtK069y9rNdig==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '>=8.51.0' - - eslint-plugin-de-morgan@2.1.2: - resolution: {integrity: sha512-6/MXP77FUrFGTdJ2Lny3Jv027bE3SUHDf+/VBUBckpH5TskOY5G4UX+AebqJeUpt3nXjy3S0KWBRwTItVQwgvw==} - engines: {node: ^20.0.0 || >=22.0.0} - peerDependencies: - eslint: ^8.45.0 || ^9.0.0 || ^10.0.0 - - eslint-plugin-escompat@3.11.4: - resolution: {integrity: sha512-j0ywwNnIufshOzgAu+PfIig1c7VRClKSNKzpniMT2vXQ4leL5q+e/SpMFQU0nrdL2WFFM44XmhSuwmxb3G0CJg==} - peerDependencies: - eslint: '>=5.14.1' - - eslint-plugin-eslint-comments@3.2.0: - resolution: {integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==} - engines: {node: '>=6.5.0'} - peerDependencies: - eslint: '>=4.19.1' - - eslint-plugin-filenames@1.3.2: - resolution: {integrity: sha512-tqxJTiEM5a0JmRCUYQmxw23vtTxrb2+a3Q2mMOPhFxvt7ZQQJmdiuMby9B/vUAuVMghyP7oET+nIf6EO6CBd/w==} - peerDependencies: - eslint: '*' - - eslint-plugin-github@6.0.0: - resolution: {integrity: sha512-J8MvUoiR/TU/Y9NnEmg1AnbvMUj9R6IO260z47zymMLLvso7B4c80IKjd8diqmqtSmeXXlbIus4i0SvK84flag==} - hasBin: true - peerDependencies: - eslint: ^8 || ^9 - - eslint-plugin-i18n-text@1.0.1: - resolution: {integrity: sha512-3G3UetST6rdqhqW9SfcfzNYMpQXS7wNkJvp6dsXnjzGiku6Iu5hl3B0kmk6lIcFPwYjhQIY+tXVRtK9TlGT7RA==} - peerDependencies: - eslint: '>=5.0.0' - - eslint-plugin-import-x@4.16.2: - resolution: {integrity: sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==} + eslint-plugin-import-x@4.17.1: + resolution: {integrity: sha512-4cdstYkKCyjumM2Q9NSI03K8D2a9F4Ssz33K2lv2hQa4KmR9jPLwk3uWGtNvclfqBrPGfGuMBwsGMbe6dMRbfg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/utils': ^8.56.0 @@ -2638,52 +2468,28 @@ packages: '@typescript-eslint/parser': optional: true - eslint-plugin-jsx-a11y@6.10.2: - resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} - engines: {node: '>=4.0'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - - eslint-plugin-no-only-tests@3.4.0: - resolution: {integrity: sha512-4S3/9Nb7A2tiMcpzEQE9bQSlpeOz6WJkgryBuou/SA8W2x2c8Zf4j0NvTKBjv6qNhF9T79tmkecm/0CHqV0UGg==} - engines: {node: '>=5.0.0'} - - eslint-plugin-playwright@2.10.4: - resolution: {integrity: sha512-l0V/VxyqfFbtqCTxj5AdRn3Q6S/hIW4nKBnKZVleVbZ24N2My6Usj//ytX3dKKqAoSbvKck9YtSytfdZ5qjLuA==} + eslint-plugin-playwright@2.10.5: + resolution: {integrity: sha512-4k+ml4cEd55kePUIW1WsCiOLDwZkAdPZPKMFulR83XpplCaVOTKHF6yvXLYixLtdVbkMjmKV5F3BaGuI3aH8aQ==} engines: {node: '>=16.9.0'} peerDependencies: eslint: '>=8.40.0' - eslint-plugin-prettier@5.5.6: - resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - '@types/eslint': '>=8.0.0' - eslint: '>=8.0.0' - eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' - prettier: '>=3.0.0' - peerDependenciesMeta: - '@types/eslint': - optional: true - eslint-config-prettier: - optional: true - - eslint-plugin-regexp@3.1.0: - resolution: {integrity: sha512-qGXIC3DIKZHcK1H9A9+Byz9gmndY6TTSRkSMTZpNXdyCw2ObSehRgccJv35n9AdUakEjQp5VFNLas6BMXizCZg==} + eslint-plugin-regexp@3.1.1: + resolution: {integrity: sha512-MxR5nqoQCtVWmJwia0D2+NlXX1xzdpkslsVOZLEYQ4PQWEaL65PCZXURxaBc3lPnkNFpNxzMIRmYVxdl8giXRA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} peerDependencies: eslint: '>=9.38.0' - eslint-plugin-sonarjs@4.0.3: - resolution: {integrity: sha512-5drkJKLC9qQddIiaATV0e8+ygbUc7b0Ti6VB7M2d3jmKNh3X0RaiIJYTs3dr9xnlhlrxo+/s1FoO3Jgv6O/c7g==} + eslint-plugin-sonarjs@4.1.0: + resolution: {integrity: sha512-rh+FlVz0yfd2RNIb6WqSkuGh0addX/Qi5scwQ5FphXDFrM6fZKcxP1+attJ78yUKcyYfiu6MTaISPpAFPzqRJw==} peerDependencies: eslint: ^8.0.0 || ^9.0.0 || ^10.0.0 - eslint-plugin-unicorn@64.0.0: - resolution: {integrity: sha512-rNZwalHh8i0UfPlhNwg5BTUO1CMdKNmjqe+TgzOTZnpKoi8VBgsW7u9qCHIdpxEzZ1uwrJrPF0uRb7l//K38gA==} - engines: {node: ^20.10.0 || >=21.0.0} + eslint-plugin-unicorn@71.1.0: + resolution: {integrity: sha512-dn3YmR3qLLUeYyo/os3ubZ7UHQJ1WbBAgC9cIhnLTyMj9J6kivuc2U1fCmYetLexUlTDVYtBqhjSj/VaebTe6Q==} + engines: {node: '>=22'} peerDependencies: - eslint: '>=9.38.0' + eslint: '>=10.4' eslint-plugin-vue-scoped-css@3.1.1: resolution: {integrity: sha512-GIskMvLPnDtiu88rWXQHy2b2QZ4j959N5UgghML64jH0sg3Km+HRa9m7nkpcEBGLD4iA4vtMDbBIoLdFcbT8lQ==} @@ -2718,10 +2524,6 @@ packages: peerDependencies: eslint: '>=8.40.0' - eslint-rule-documentation@1.0.23: - resolution: {integrity: sha512-pWReu3fkohwyvztx/oQWWgld2iad25TfUdi6wvhhaDPIQjHU/pyvlKgXFw1kX31SQK2Nq9MH+vRDWB0ZLy8fYw==} - engines: {node: '>=4.0.0'} - eslint-scope@9.1.2: resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -2738,8 +2540,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.4.1: - resolution: {integrity: sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==} + eslint@10.6.0: + resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -2783,34 +2585,13 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - eventemitter3@2.0.3: - resolution: {integrity: sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==} - - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - - exit-hook@1.1.1: - resolution: {integrity: sha512-MsG3prOVw1WtLXAZbM3KiYtooKR1LvxHh3VHsVtIy0uiUu8usxgB/94DP2HxtD/661lLdB6yzQ09lGJSQr6nkg==} - engines: {node: '>=0.10.0'} - expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - - extsprintf@1.3.0: - resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} - engines: {'0': node >=0.6.0} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -2846,10 +2627,6 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} - figures@1.7.0: - resolution: {integrity: sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==} - engines: {node: '>=0.10.0'} - file-entry-cache@11.1.3: resolution: {integrity: sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==} @@ -2883,27 +2660,10 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - forever-agent@0.6.1: - resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} - - form-data@2.3.3: - resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} - engines: {node: '>= 0.12'} - form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} - fs-extra@0.26.7: - resolution: {integrity: sha512-waKu+1KumRhYv8D8gMRCKJGAMI9pRnPuEb1mvgYD0f7wBscg+h6bW4FDTmEZhB9VKxvoTtxW+Y7bnIlB7zja6Q==} - - fs-promise@0.5.0: - resolution: {integrity: sha512-Y+4F4ujhEcayCJt6JmzcOun9MYGQwz+bVUiuBmTkJImhBHKpBvmVPZR9wtfiF7k3ffwAOAuurygQe+cPLSFQhw==} - deprecated: Use mz or fs-extra^3.0 with Promise Support - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2917,6 +2677,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + function-timeout@1.0.2: + resolution: {integrity: sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==} + engines: {node: '>=18'} + function.prototype.name@1.1.8: resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} engines: {node: '>= 0.4'} @@ -2950,9 +2714,6 @@ packages: get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} - getpass@0.1.7: - resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2961,10 +2722,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - global-modules@2.0.0: resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} engines: {node: '>=6'} @@ -2973,16 +2730,8 @@ packages: resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} engines: {node: '>=6'} - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - - globals@16.5.0: - resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} - engines: {node: '>=18'} - - globals@17.6.0: - resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + globals@17.7.0: + resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} engines: {node: '>=18'} globalthis@1.0.4: @@ -3010,23 +2759,10 @@ packages: resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} engines: {node: '>=0.8.0'} - happy-dom@20.10.2: - resolution: {integrity: sha512-5p9Sxis3eowDJKqx90QCsgbNA02XXqJ59NOHvD4V6cxp+rP4d/xOyVx7uY3hS8hiUbY1VeiFH8lbJ81AyuDVLQ==} + happy-dom@20.10.6: + resolution: {integrity: sha512-6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw==} engines: {node: '>=20.0.0'} - har-schema@2.0.0: - resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} - engines: {node: '>=4'} - - har-validator@5.1.5: - resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} - engines: {node: '>=6'} - deprecated: this library is no longer supported - - has-ansi@2.0.0: - resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} - engines: {node: '>=0.10.0'} - has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -3083,10 +2819,6 @@ packages: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} - http-signature@1.2.0: - resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} - engines: {node: '>=0.8', npm: '>=1.3.7'} - https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -3095,6 +2827,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + identifier-regex@1.1.0: + resolution: {integrity: sha512-SLX4H/vtcYlYnL7XqnuJKHU7Z8517TgsW9nmQiGOgMCjQ8V/deLYu6bEmbGoXe7WMMhc9+EUGyFFneHja8KabA==} + engines: {node: '>=18'} + idiomorph@0.7.4: resolution: {integrity: sha512-uCdSpLo3uMfqOmrwXTpR1k/sq4sSmKC7l4o/LdJOEU+MMMq+wkevRqOQYn3lP7vfz9Mv+USBEqPvi0XhdL9ENw==} @@ -3124,13 +2860,6 @@ packages: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} @@ -3138,12 +2867,6 @@ packages: resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - inquirer-promise@0.0.3: - resolution: {integrity: sha512-82CQX586JAV9GAgU9yXZsMDs+NorjA0nLhkfFx9+PReyOnuoHRbHrC1Z90sS95bFJI1Tm1gzMObuE0HabzkJpg==} - - inquirer@0.11.4: - resolution: {integrity: sha512-QR+2TW90jnKk9LUUtbcA3yQXKt2rDEKMh6+BAZQIeumtzHexnwVLdPakSslGijXYLJCzFv7GMXbFCn0pA00EUw==} - internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -3184,9 +2907,6 @@ packages: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} - is-buffer@1.1.6: - resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} - is-builtin-module@5.0.0: resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} engines: {node: '>=18.20'} @@ -3221,10 +2941,6 @@ packages: resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} engines: {node: '>= 0.4'} - is-fullwidth-code-point@1.0.0: - resolution: {integrity: sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==} - engines: {node: '>=0.10.0'} - is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -3240,6 +2956,10 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-identifier@1.1.0: + resolution: {integrity: sha512-NhOds0mDx9lJu+1lBRO0xbwFo5nobA7GCk/0e5xjr6+6XugX985+0OyGX35BNrTkPAsdLcIKg02HUQJOK8D8kw==} + engines: {node: '>=18'} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -3287,9 +3007,6 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} - is-typedarray@1.0.0: - resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - is-valid-element-name@1.0.0: resolution: {integrity: sha512-GZITEJY2LkSjQfaIPBha7eyZv+ge0PhBR7KITeCCWvy7VBQrCUdFkvpI+HrAPQjVtVjy1LvlEkqQTHckoszruw==} @@ -3311,9 +3028,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isstream@0.1.2: - resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} - jest-environment-jsdom@29.7.0: resolution: {integrity: sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3355,16 +3069,13 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - js-yaml@4.2.0: resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true - jsbn@0.1.1: - resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + js-yaml@5.2.1: + resolution: {integrity: sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==} + hasBin: true jsdoc-type-pratt-parser@7.2.0: resolution: {integrity: sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==} @@ -3396,15 +3107,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json-schema@0.4.0: - resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} - json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -3412,28 +3117,14 @@ packages: jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - jsonfile@2.4.0: - resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==} - jsonpointer@5.0.1: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} - jsprim@1.4.2: - resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} - engines: {node: '>=0.6.0'} - jsx-ast-utils-x@0.1.0: resolution: {integrity: sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - jsx-ast-utils@3.3.5: - resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} - engines: {node: '>=4.0'} - - kaiser@0.0.4: - resolution: {integrity: sha512-m8ju+rmBqvclZmyrOXgGGhOYSjKJK6RN1NhqEltemY87UqZOxEkizg9TOy1vQSyJ01Wx6SAPuuN0iO2Mgislvw==} - katex@0.16.47: resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} hasBin: true @@ -3451,24 +3142,10 @@ packages: khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} - kind-of@3.2.2: - resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} - engines: {node: '>=0.10.0'} - kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} - klaw@1.3.1: - resolution: {integrity: sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==} - - language-subtag-registry@0.3.23: - resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} - - language-tags@1.0.9: - resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} - engines: {node: '>=0.10'} - layout-base@1.0.2: resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} @@ -3573,45 +3250,31 @@ packages: lodash-es@4.18.1: resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} - lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - - lodash.kebabcase@4.1.1: - resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.snakecase@4.1.1: - resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} - lodash.truncate@4.4.2: resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} - lodash.upperfirst@4.3.1: - resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} - - lodash@3.10.1: - resolution: {integrity: sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ==} - - lodash@4.18.1: - resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - markdown-it@14.1.1: - resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + make-asynchronous@1.1.0: + resolution: {integrity: sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==} + engines: {node: '>=18'} + + markdown-it@14.2.0: + resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==} hasBin: true - markdownlint-cli@0.48.0: - resolution: {integrity: sha512-NkZQNu2E0Q5qLEEHwWj674eYISTLD4jMHkBzDobujXd1kv+yCxi8jOaD/rZoQNW1FBBMMGQpuW5So8B51N/e0A==} - engines: {node: '>=20'} + markdownlint-cli@0.49.0: + resolution: {integrity: sha512-vS5tWq5W91Gg33LD4pyAaXPclnz/sRvo6/RGOyDQjQ3eds2DkK6H4szUuE0M9TiRB/u/VBx1gtd9Ktrtx5WlSA==} + engines: {node: '>=22'} hasBin: true - markdownlint@0.40.0: - resolution: {integrity: sha512-UKybllYNheWac61Ia7T6fzuQNDZimFIpCg2w6hHjgV1Qu0w1TV0LlSgryUGzM0bkKQCBhy2FDhEELB73Kb0kAg==} - engines: {node: '>=20'} + markdownlint@0.41.0: + resolution: {integrity: sha512-xMUI3ChBuRuxuLF4ENvCZyS8z/+Jly1coUcZwErKLIB3sDj7ojpaTBa1e9YVPhSN4jGEIjYGQCldbTJS/hqS+A==} + engines: {node: '>=22'} marked@16.4.2: resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} @@ -3623,8 +3286,8 @@ packages: engines: {node: '>= 12'} hasBin: true - material-icon-theme@5.35.0: - resolution: {integrity: sha512-ptU3rjmZjPCeLRog0HcJ1HtnoVgOslc350WHk1oIvX7fVFE4NBAF7GL3QvCCEjtd1TSSDoxmS4dWsv6bTB1x9g==} + material-icon-theme@5.36.1: + resolution: {integrity: sha512-m61BOQlbzno5KrDMuSb2Z+SxPYXWrVSRZRo6YtmoboZjFdP+HMMAzl6fGlKZUkX7dDBV++eDXYOx5hAsr7derA==} engines: {vscode: ^1.55.0} math-intrinsics@1.1.0: @@ -3651,8 +3314,8 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - mermaid@11.15.0: - resolution: {integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==} + mermaid@11.16.0: + resolution: {integrity: sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==} micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -3760,9 +3423,6 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - mute-stream@0.0.5: - resolution: {integrity: sha512-EbrziT4s8cWPmzr47eYVW3wimS4HsvlnV5ri1xw1aR6JQo/OrJX5rkl32K/QQHdxeabJETtfeaROGhd8W7uBgg==} - mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -3808,6 +3468,10 @@ packages: resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} engines: {node: '>=18'} + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + engines: {node: '>=18'} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -3815,16 +3479,9 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - number-is-nan@1.0.1: - resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} - engines: {node: '>=0.10.0'} - nwsapi@2.2.23: resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} - oauth-sign@0.9.0: - resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3864,13 +3521,6 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@1.1.0: - resolution: {integrity: sha512-GZ+g4jayMqzCRMgB2sol7GiCLjKfS1PINkjmx8spcKce1LiVqcbQreXwqs2YAFXC6R03VIG28ZS31t8M866v6A==} - engines: {node: '>=0.10.0'} - online-3d-viewer@0.18.0: resolution: {integrity: sha512-y7ZlV/zkakNUyjqcXz6XecA7vXgLEUnaAey9tyx8o6/wcdV64RfjXAQOjGXGY2JOZoDi4Cg1ic9icSWMWAvRQA==} @@ -3878,14 +3528,14 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - os-homedir@1.0.2: - resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} - engines: {node: '>=0.10.0'} - own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + p-event@6.0.1: + resolution: {integrity: sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==} + engines: {node: '>=16.17'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -3894,6 +3544,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-timeout@6.1.4: + resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} + engines: {node: '>=14.16'} + package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} @@ -3921,10 +3575,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -3941,9 +3591,6 @@ packages: perfect-debounce@2.1.0: resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} - performance-now@2.1.0: - resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3951,8 +3598,8 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} pify@2.3.0: @@ -3963,13 +3610,13 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - playwright-core@1.60.0: - resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} engines: {node: '>=18'} hasBin: true - playwright@1.60.0: - resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} engines: {node: '>=18'} hasBin: true @@ -4043,30 +3690,21 @@ packages: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} - postcss-selector-parser@7.1.1: - resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + postcss-selector-parser@7.1.4: + resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} engines: {node: '>=4'} postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-linter-helpers@1.0.1: - resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} - engines: {node: '>=6.0.0'} - - prettier@3.8.3: - resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} - engines: {node: '>=14'} - hasBin: true - pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4086,16 +3724,16 @@ packages: resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} engines: {node: '>=20'} - qs@6.5.5: - resolution: {integrity: sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==} - engines: {node: '>=0.6'} - querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quote-js-string@0.1.0: + resolution: {integrity: sha512-Y3NoRtprEEZQD8RfxMCfS0ZTqc4e+i18OrXEXAvpM6TfC/3y+0L5rNbZiSnbBBEkDfFzbpd8o+cE8q3/anjMGA==} + engines: {node: '>=22'} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -4106,9 +3744,6 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - readline2@1.0.1: - resolution: {integrity: sha512-8/td4MmwUB6PkZUbV25uKz7dfrmjYWxsW8DVfibWdlHRk/l/DfHKn4pU+dfcoGLFgWOdyGCzINRQD7jn+Bv+/g==} - refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -4117,39 +3752,18 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} - regenerator-runtime@0.9.6: - resolution: {integrity: sha512-D0Y/JJ4VhusyMOd/o25a3jdUqN/bC85EFsaoL9Oqmy/O4efCh+xhp7yj2EEOsj974qvMkcW8AwUzJ1jB/MbxCw==} - regexp-ast-analysis@0.7.1: resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - regexp-tree@0.1.27: - resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} - hasBin: true - regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} - regjsparser@0.13.1: - resolution: {integrity: sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==} + regjsparser@0.13.2: + resolution: {integrity: sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==} hasBin: true - rename-keys@1.2.0: - resolution: {integrity: sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==} - engines: {node: '>= 0.8.0'} - - request-promise@3.0.0: - resolution: {integrity: sha512-wVGUX+BoKxYsavTA72i6qHcyLbjzM4LR4y/AmDCqlbuMAursZdDWO7PmgbGAUvD2SeEJ5iB99VSq/U51i/DNbw==} - engines: {node: '>=0.10.0'} - deprecated: request-promise has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142 - - request@2.88.2: - resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} - engines: {node: '>= 6'} - deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 - require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -4157,6 +3771,10 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + reserved-identifiers@1.2.0: + resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} + engines: {node: '>=18'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -4174,19 +3792,10 @@ packages: engines: {node: '>= 0.4'} hasBin: true - restore-cursor@1.0.1: - resolution: {integrity: sha512-reSjH4HuiFlxlaBaFCiS6O76ZGG2ygKoSlCsipKdaZuKSPx/+bt9mULkn4l0asVzbEfQQmXRg6Wp6gv6m0wElw==} - engines: {node: '>=0.10.0'} - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rimraf@2.7.1: - resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} @@ -4195,17 +3804,14 @@ packages: peerDependencies: rolldown: '*' - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} - run-async@0.1.0: - resolution: {integrity: sha512-qOX+w+IxFgpUpJfkv2oGN0+ExPs68F4sZHfaRRx4dDexAQkG83atugKVEylyT5ARees3HBbfmuvnjbrd8j9Wjw==} - run-con@1.3.2: resolution: {integrity: sha512-CcfE+mYiTcKEzg0IqS08+efdnH0oJ3zV0wSUFBNrMHMuxCtXvBCLzCJHatwuXDcu/RlhjTziTo/a1ruQik6/Yg==} hasBin: true @@ -4216,16 +3822,10 @@ packages: rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - rx-lite@3.1.2: - resolution: {integrity: sha512-1I1+G2gteLB8Tkt8YI1sJvSIfa0lWuRtC8GjvtyPBcLSF5jBCCJJqKrpER5JU5r6Bhe+i9/pK3VMuUcXu0kdwQ==} - safe-array-concat@1.1.4: resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -4253,8 +3853,13 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.8.1: - resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true @@ -4347,16 +3952,11 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - spectral-cli-bundle@1.0.8: - resolution: {integrity: sha512-wxHPXFy5bdtqT/flJRjWbSdPoDjIiXzmLvxE7RO9bbs/B2DuavW2UIG8HYa7m7F6RlQaK7rhoiIRoYEEeBP5Jg==} + spectral-cli-bundle@1.0.11: + resolution: {integrity: sha512-BOAZHva6LK/VCO4vcKiGnEcAjhx88RQqE9mVIG5EpT+4Nz3trydR2wtK00pOley7/JfOh4b7J22rRYgCbAl47Q==} engines: {node: '>=20'} hasBin: true - sshpk@1.18.0: - resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} - engines: {node: '>=0.10.0'} - hasBin: true - stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -4375,26 +3975,14 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - string-width@1.0.2: - resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==} - engines: {node: '>=0.10.0'} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} - string-width@8.1.0: - resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==} - engines: {node: '>=20'} - string-width@8.2.1: resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} engines: {node: '>=20'} - string.prototype.includes@2.0.1: - resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} - engines: {node: '>= 0.4'} - string.prototype.trim@1.2.10: resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} engines: {node: '>= 0.4'} @@ -4407,10 +3995,6 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} - strip-ansi@3.0.1: - resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} - engines: {node: '>=0.10.0'} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -4461,8 +4045,8 @@ packages: peerDependencies: stylelint: '>=16' - stylelint@17.13.0: - resolution: {integrity: sha512-G1WYzMerp7ihOaIe9VJCHLt12MoAD2QLf1AFerYP37+BCRBUK5UCpq8e/mN+zCIaJPKQcaxhE4WlPmqdiOx/gw==} + stylelint@17.14.0: + resolution: {integrity: sha512-8xkHPpdqYryeIsOgfsYTmr6cIeC4nLYWk5S8BPxpodq8mIuepggkMljsHewWfuAjj/+qpRKou2QerhjMH3iasg==} engines: {node: '>=20.19.0'} hasBin: true @@ -4474,14 +4058,14 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + super-regex@1.1.0: + resolution: {integrity: sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==} + engines: {node: '>=18'} + supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} - supports-color@2.0.0: - resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} - engines: {node: '>=0.8.0'} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -4494,9 +4078,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svg-element-attributes@1.3.1: - resolution: {integrity: sha512-Bh05dSOnJBf3miNMqpsormfNtfidA/GxQVakhtn0T4DECWKeXQRQUceYjJ+OxYiiLdGe4Jo9iFV8wICFapFeIA==} - svg-tags@1.0.0: resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} @@ -4505,11 +4086,8 @@ packages: engines: {node: '>=16'} hasBin: true - svgson@5.3.1: - resolution: {integrity: sha512-qdPgvUNWb40gWktBJnbJRelWcPzkLed/ShhnRsjbayXz8OtdPOzbil9jtiZdrYvSDumAz/VNQr6JaNfPx/gvPA==} - - swagger-ui-dist@5.32.6: - resolution: {integrity: sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==} + swagger-ui-dist@5.32.8: + resolution: {integrity: sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==} symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -4518,10 +4096,6 @@ packages: resolution: {integrity: sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==} engines: {node: '>=14'} - synckit@0.11.13: - resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} - engines: {node: ^14.18.0 || >=16.0.0} - table@6.9.0: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} @@ -4545,8 +4119,9 @@ packages: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} - through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + time-span@5.1.0: + resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==} + engines: {node: '>=12'} tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -4573,10 +4148,6 @@ packages: toastify-js@1.12.0: resolution: {integrity: sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==} - tough-cookie@2.5.0: - resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} - engines: {node: '>=0.8'} - tough-cookie@4.1.4: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} @@ -4610,12 +4181,6 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tunnel-agent@0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - - tweetnacl@0.14.5: - resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} - type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -4624,6 +4189,10 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -4640,18 +4209,13 @@ packages: resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} engines: {node: '>= 0.4'} - typescript-eslint@8.61.0: - resolution: {integrity: sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==} + typescript-eslint@8.63.0: + resolution: {integrity: sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -4670,8 +4234,8 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@7.24.6: - resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} unicorn-magic@0.4.0: resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} @@ -4684,18 +4248,14 @@ packages: unrs-resolver@1.12.2: resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} - untildify@3.0.3: - resolution: {integrity: sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==} - engines: {node: '>=4'} - update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - updates@17.18.0: - resolution: {integrity: sha512-LrTEX4jauR3fMVnni6JtcD1fkbIjxtvJhNXjASZfBFFEaNF3FDWjw6bcenymevduP3zuFortJMIbGFznZeSXtA==} + updates@17.18.2: + resolution: {integrity: sha512-pDXRt93EdgSA45L7lNXO7WcE+Zfk4FOPGPJDg/qbAqPubtGX1ORcPVxlUKWXB2ZfIPOOV1I7KrJOpw0uE0dKPA==} engines: {node: '>=22'} hasBin: true @@ -4705,10 +4265,6 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - user-home@2.0.0: - resolution: {integrity: sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ==} - engines: {node: '>=0.10.0'} - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -4716,30 +4272,21 @@ packages: resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} hasBin: true - uuid@3.4.0: - resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). - hasBin: true - vanilla-colorful@0.7.2: resolution: {integrity: sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg==} - verror@1.10.0: - resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} - engines: {'0': node >=0.6.0} - - vite-string-plugin@2.0.4: - resolution: {integrity: sha512-uahQl15I6hsHGzbjCmllVoKZBmZavXAp8GXBmq5omNQM52wgBv7e//98A6cONbq0Of6F/5uJ30OjF5ggNY6reQ==} + vite-string-plugin@2.0.5: + resolution: {integrity: sha512-b4F5xI6zAw+LTbRT2RlPKqdBneHLB9P5aqW9aW7z4zK9/nUEG7Hc8Ml1K2Zwmc0xE0s9k01JcnK+xCFYXLGmZA==} peerDependencies: vite: '*' - vite@8.0.16: - resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + vite@8.1.4: + resolution: {integrity: sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 + '@vitejs/devtools': ^0.3.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -4776,20 +4323,20 @@ packages: yaml: optional: true - vitest@4.1.8: - resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.8 - '@vitest/browser-preview': 4.1.8 - '@vitest/browser-webdriverio': 4.1.8 - '@vitest/coverage-istanbul': 4.1.8 - '@vitest/coverage-v8': 4.1.8 - '@vitest/ui': 4.1.8 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -4820,11 +4367,8 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - vue-bar-graph@2.2.0: - resolution: {integrity: sha512-1xFPho2nM6nFDziExLu48vKO+Q90gjxz1NyHfc+MhgfYDSxR9BMyhOIXUO5EmwKIVEX5dBoP2n3Ius8SjKRD4g==} - - vue-chartjs@5.3.3: - resolution: {integrity: sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==} + vue-chartjs@5.3.4: + resolution: {integrity: sha512-x3Fqob8RQvrTdssfi9ecsCzEkFOd8JPmNwSkSQzdfKj/uBsRJs/Y88cZcZIEcPsTVfMGwMo4MOoihoDG2DoE/g==} peerDependencies: chart.js: ^4.1.1 vue: ^3.0.0-0 || ^2.7.0 @@ -4835,14 +4379,14 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - vue-tsc@3.3.4: - resolution: {integrity: sha512-XA/JqmQwS2GZmfgpjOEGdrKwaTSEuPwxpHa7/t6f4yiGrJb3gVHTPb9wBfByMNZwQ+xDXs41b8gaS2DKsOozUw==} + vue-tsc@3.3.7: + resolution: {integrity: sha512-+C+rgD49wAQ5bUTl2sp5a8Bzg4YoldMNXM+g7CFe604MYcQ8PrZPMQhIjJSzKXtPBCa+C5ayMipqjbA7splekQ==} hasBin: true peerDependencies: typescript: '>=5.0.0' - vue@3.5.37: - resolution: {integrity: sha512-So4bMq165gsD4+hDVC1/bcbsOpTlUFGGkpuH2sx9vCflChIWahy4C0K4ZcX/COe0ad1IToIRT3VQz0dl4XPihg==} + vue@3.5.39: + resolution: {integrity: sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -4856,6 +4400,9 @@ packages: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} + web-worker@1.5.0: + resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -4913,9 +4460,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - write-file-atomic@7.0.1: resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} @@ -4932,19 +4476,18 @@ packages: utf-8-validate: optional: true - xml-lexer@0.2.2: - resolution: {integrity: sha512-G0i98epIwiUEiKmMcavmVdhtymW+pCAohMRgybyIME9ygfVu8QheIi+YoQh3ngiThsT0SQzJT4R0sKDEv8Ou0w==} - xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} - xml-reader@2.4.3: - resolution: {integrity: sha512-xWldrIxjeAMAu6+HSf9t50ot1uL5M+BtOidRCWHXIeewvSeIpscWCsp4Zxjk8kHHhdqFBrfK8U0EJeCcnyQ/gA==} - xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -5055,45 +4598,45 @@ snapshots: '@codemirror/autocomplete@6.20.3': dependencies: - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 '@lezer/common': 1.5.2 - '@codemirror/commands@6.10.3': + '@codemirror/commands@6.10.4': dependencies: - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 '@lezer/common': 1.5.2 '@codemirror/lang-angular@0.1.4': dependencies: '@codemirror/lang-html': 6.4.11 '@codemirror/lang-javascript': 6.2.5 - '@codemirror/language': 6.12.3 + '@codemirror/language': 6.12.4 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 '@codemirror/lang-cpp@6.0.3': dependencies: - '@codemirror/language': 6.12.3 + '@codemirror/language': 6.12.4 '@lezer/cpp': 1.1.6 '@codemirror/lang-css@6.3.1': dependencies: '@codemirror/autocomplete': 6.20.3 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 '@lezer/common': 1.5.2 '@lezer/css': 1.3.3 '@codemirror/lang-go@6.0.1': dependencies: '@codemirror/autocomplete': 6.20.3 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 '@lezer/common': 1.5.2 '@lezer/go': 1.0.1 @@ -5102,25 +4645,25 @@ snapshots: '@codemirror/autocomplete': 6.20.3 '@codemirror/lang-css': 6.3.1 '@codemirror/lang-javascript': 6.2.5 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 '@lezer/common': 1.5.2 '@lezer/css': 1.3.3 '@lezer/html': 1.3.13 '@codemirror/lang-java@6.0.2': dependencies: - '@codemirror/language': 6.12.3 + '@codemirror/language': 6.12.4 '@lezer/java': 1.1.3 '@codemirror/lang-javascript@6.2.5': dependencies: '@codemirror/autocomplete': 6.20.3 - '@codemirror/language': 6.12.3 + '@codemirror/language': 6.12.4 '@codemirror/lint': 6.9.7 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 '@lezer/common': 1.5.2 '@lezer/javascript': 1.5.4 @@ -5128,22 +4671,22 @@ snapshots: dependencies: '@codemirror/autocomplete': 6.20.3 '@codemirror/lang-html': 6.4.11 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 '@codemirror/lang-json@6.0.2': dependencies: - '@codemirror/language': 6.12.3 + '@codemirror/language': 6.12.4 '@lezer/json': 1.0.3 '@codemirror/lang-less@6.0.2': dependencies: '@codemirror/lang-css': 6.3.1 - '@codemirror/language': 6.12.3 + '@codemirror/language': 6.12.4 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 @@ -5152,9 +4695,9 @@ snapshots: dependencies: '@codemirror/autocomplete': 6.20.3 '@codemirror/lang-html': 6.4.11 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 @@ -5163,46 +4706,46 @@ snapshots: dependencies: '@codemirror/autocomplete': 6.20.3 '@codemirror/lang-html': 6.4.11 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 '@lezer/common': 1.5.2 '@lezer/markdown': 1.6.4 '@codemirror/lang-php@6.0.2': dependencies: '@codemirror/lang-html': 6.4.11 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 '@lezer/common': 1.5.2 '@lezer/php': 1.0.5 '@codemirror/lang-python@6.2.1': dependencies: '@codemirror/autocomplete': 6.20.3 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 '@lezer/common': 1.5.2 '@lezer/python': 1.1.19 '@codemirror/lang-rust@6.0.2': dependencies: - '@codemirror/language': 6.12.3 + '@codemirror/language': 6.12.4 '@lezer/rust': 1.0.2 '@codemirror/lang-sass@6.0.2': dependencies: '@codemirror/lang-css': 6.3.1 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 '@lezer/common': 1.5.2 '@lezer/sass': 1.1.0 '@codemirror/lang-sql@6.10.0': dependencies: '@codemirror/autocomplete': 6.20.3 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 @@ -5211,14 +4754,14 @@ snapshots: dependencies: '@codemirror/lang-html': 6.4.11 '@codemirror/lang-javascript': 6.2.5 - '@codemirror/language': 6.12.3 + '@codemirror/language': 6.12.4 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 '@codemirror/lang-wast@6.0.2': dependencies: - '@codemirror/language': 6.12.3 + '@codemirror/language': 6.12.4 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 @@ -5226,17 +4769,17 @@ snapshots: '@codemirror/lang-xml@6.1.0': dependencies: '@codemirror/autocomplete': 6.20.3 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 '@lezer/common': 1.5.2 '@lezer/xml': 1.0.6 '@codemirror/lang-yaml@6.1.3': dependencies: '@codemirror/autocomplete': 6.20.3 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 @@ -5265,13 +4808,13 @@ snapshots: '@codemirror/lang-wast': 6.0.2 '@codemirror/lang-xml': 6.1.0 '@codemirror/lang-yaml': 6.1.3 - '@codemirror/language': 6.12.3 + '@codemirror/language': 6.12.4 '@codemirror/legacy-modes': 6.5.3 - '@codemirror/language@6.12.3': + '@codemirror/language@6.12.4': dependencies: - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 @@ -5279,27 +4822,27 @@ snapshots: '@codemirror/legacy-modes@6.5.3': dependencies: - '@codemirror/language': 6.12.3 + '@codemirror/language': 6.12.4 '@codemirror/lint@6.9.7': dependencies: - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 crelt: 1.0.6 - '@codemirror/search@6.7.0': + '@codemirror/search@6.7.1': dependencies: - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 crelt: 1.0.6 - '@codemirror/state@6.6.0': + '@codemirror/state@6.7.1': dependencies: '@marijn/find-cluster-break': 1.0.2 - '@codemirror/view@6.43.1': + '@codemirror/view@6.43.6': dependencies: - '@codemirror/state': 6.6.0 + '@codemirror/state': 6.7.1 crelt: 1.0.6 style-mod: 4.1.3 w3c-keyname: 2.2.8 @@ -5313,7 +4856,7 @@ snapshots: dependencies: '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.4(css-tree@3.2.1)': + '@csstools/css-syntax-patches-for-csstree@1.1.6(css-tree@3.2.1)': optionalDependencies: css-tree: 3.2.1 @@ -5324,13 +4867,13 @@ snapshots: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/selector-resolve-nested@4.0.0(postcss-selector-parser@7.1.1)': + '@csstools/selector-resolve-nested@4.0.0(postcss-selector-parser@7.1.4)': dependencies: - postcss-selector-parser: 7.1.1 + postcss-selector-parser: 7.1.4 - '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.1)': + '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.4)': dependencies: - postcss-selector-parser: 7.1.1 + postcss-selector-parser: 7.1.4 '@deltablot/dropzone@7.4.3': dependencies: @@ -5342,16 +4885,32 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.28.1': optional: true @@ -5430,25 +4989,19 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.7.2(eslint@10.4.1(jiti@2.7.0))': + '@eslint-community/eslint-plugin-eslint-comments@4.7.2(eslint@10.6.0(jiti@2.7.0))': dependencies: escape-string-regexp: 4.0.0 - eslint: 10.4.1(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) ignore: 7.0.5 - '@eslint-community/eslint-utils@4.9.1(eslint@10.4.1(jiti@2.7.0))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0(jiti@2.7.0))': dependencies: - eslint: 10.4.1(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@1.4.1(eslint@10.4.1(jiti@2.7.0))': - dependencies: - '@eslint/core': 0.17.0 - optionalDependencies: - eslint: 10.4.1(jiti@2.7.0) - '@eslint/config-array@0.23.5': dependencies: '@eslint/object-schema': 3.0.5 @@ -5461,31 +5014,11 @@ snapshots: dependencies: '@eslint/core': 1.2.1 - '@eslint/core@0.17.0': - dependencies: - '@types/json-schema': 7.0.15 - '@eslint/core@1.2.1': dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.5': - dependencies: - ajv: 6.15.0 - debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.2.0 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.39.4': {} - - '@eslint/json@2.0.0': + '@eslint/json@2.0.1': dependencies: '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.2 @@ -5499,8 +5032,6 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@github/browserslist-config@1.0.0': {} - '@github/combobox-nav@2.3.1': {} '@github/markdown-toolbar-element@2.2.3': {} @@ -5542,14 +5073,14 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 25.9.3 + '@types/node': 26.1.1 jest-mock: 29.7.0 '@jest/fake-timers@29.7.0': dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 25.9.3 + '@types/node': 26.1.1 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -5563,7 +5094,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 25.9.3 + '@types/node': 26.1.1 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -5698,21 +5229,28 @@ snapshots: - supports-color - utf-8-validate - '@mermaid-js/layout-elk@0.2.1(mermaid@11.15.0)': + '@mermaid-js/layout-elk@0.2.2(mermaid@11.16.0)': dependencies: d3: 7.9.0 elkjs: 0.9.3 - mermaid: 11.15.0 + mermaid: 11.16.0 - '@mermaid-js/parser@1.1.1': + '@mermaid-js/parser@1.2.0': dependencies: '@chevrotain/types': 11.1.2 - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 optional: true '@nodelib/fs.scandir@2.1.5': @@ -5727,116 +5265,113 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@oxc-project/types@0.133.0': {} + '@oxc-project/types@0.139.0': {} - '@package-json/types@0.0.12': {} - - '@pkgr/core@0.3.6': {} - - '@playwright/test@1.60.0': + '@playwright/test@1.61.1': dependencies: - playwright: 1.60.0 + playwright: 1.61.1 '@popperjs/core@2.11.8': {} - '@primer/octicons@19.28.1': + '@primer/octicons@19.29.1': dependencies: object-assign: 4.1.1 - '@replit/codemirror-indentation-markers@6.5.3(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.1)': + '@replit/codemirror-indentation-markers@6.5.3(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.6)': dependencies: - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 - '@replit/codemirror-lang-nix@6.0.1(@codemirror/autocomplete@6.20.3)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.1)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.10)': + '@replit/codemirror-lang-nix@6.0.1(@codemirror/autocomplete@6.20.3)(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.6)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.10)': dependencies: '@codemirror/autocomplete': 6.20.3 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 - '@replit/codemirror-lang-svelte@6.0.0(@codemirror/autocomplete@6.20.3)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.1)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.10)': + '@replit/codemirror-lang-svelte@6.0.0(@codemirror/autocomplete@6.20.3)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.6)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.10)': dependencies: '@codemirror/autocomplete': 6.20.3 '@codemirror/lang-css': 6.3.1 '@codemirror/lang-html': 6.4.11 '@codemirror/lang-javascript': 6.2.5 - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/javascript': 1.5.4 '@lezer/lr': 1.4.10 - '@replit/codemirror-vscode-keymap@6.0.2(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.43.1)': + '@replit/codemirror-vscode-keymap@6.0.2(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.1)(@codemirror/view@6.43.6)': dependencies: '@codemirror/autocomplete': 6.20.3 - '@codemirror/commands': 6.10.3 - '@codemirror/language': 6.12.3 + '@codemirror/commands': 6.10.4 + '@codemirror/language': 6.12.4 '@codemirror/lint': 6.9.7 - '@codemirror/search': 6.7.0 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/search': 6.7.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 '@resvg/resvg-wasm@2.6.2': {} - '@rolldown/binding-android-arm64@1.0.3': + '@rolldown/binding-android-arm64@1.1.5': optional: true - '@rolldown/binding-darwin-arm64@1.0.3': + '@rolldown/binding-darwin-arm64@1.1.5': optional: true - '@rolldown/binding-darwin-x64@1.0.3': + '@rolldown/binding-darwin-x64@1.1.5': optional: true - '@rolldown/binding-freebsd-x64@1.0.3': + '@rolldown/binding-freebsd-x64@1.1.5': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.3': + '@rolldown/binding-linux-arm64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.3': + '@rolldown/binding-linux-arm64-musl@1.1.5': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.3': + '@rolldown/binding-linux-ppc64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.3': + '@rolldown/binding-linux-s390x-gnu@1.1.5': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.3': + '@rolldown/binding-linux-x64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-x64-musl@1.0.3': + '@rolldown/binding-linux-x64-musl@1.1.5': optional: true - '@rolldown/binding-openharmony-arm64@1.0.3': + '@rolldown/binding-openharmony-arm64@1.1.5': optional: true - '@rolldown/binding-wasm32-wasi@1.0.3': + '@rolldown/binding-wasm32-wasi@1.1.5': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.3': + '@rolldown/binding-win32-arm64-msvc@1.1.5': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.3': + '@rolldown/binding-win32-x64-msvc@1.1.5': optional: true '@rolldown/pluginutils@1.0.1': {} - '@rtsao/scc@1.1.0': {} + '@rtsao/scc@1.1.0': + optional: true '@scarf/scarf@1.4.0': {} @@ -5872,26 +5407,26 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@stylistic/eslint-plugin@5.10.0(eslint@10.4.1(jiti@2.7.0))': + '@stylistic/eslint-plugin@5.10.0(eslint@10.6.0(jiti@2.7.0))': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) - '@typescript-eslint/types': 8.61.0 - eslint: 10.4.1(jiti@2.7.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) + '@typescript-eslint/types': 8.63.0 + eslint: 10.6.0(jiti@2.7.0) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 - picomatch: 4.0.4 + picomatch: 4.0.5 - '@stylistic/stylelint-plugin@5.2.0(stylelint@17.13.0(typescript@6.0.3))': + '@stylistic/stylelint-plugin@5.2.1(stylelint@17.14.0(typescript@6.0.3))': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - postcss: 8.5.15 - postcss-selector-parser: 7.1.1 + postcss: 8.5.16 + postcss-selector-parser: 7.1.4 postcss-value-parser: 4.2.0 style-search: 0.1.0 - stylelint: 17.13.0(typescript@6.0.3) + stylelint: 17.14.0(typescript@6.0.3) '@swc/helpers@0.5.23': dependencies: @@ -5899,7 +5434,7 @@ snapshots: '@tootallnate/once@2.0.1': {} - '@tybys/wasm-util@0.10.2': + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true @@ -6056,17 +5591,16 @@ snapshots: '@types/jquery@4.0.1': {} - '@types/js-yaml@4.0.9': {} - '@types/jsdom@20.0.1': dependencies: - '@types/node': 25.9.3 + '@types/node': 26.1.1 '@types/tough-cookie': 4.0.5 parse5: 7.3.0 '@types/json-schema@7.0.15': {} - '@types/json5@0.0.29': {} + '@types/json5@0.0.29': + optional: true '@types/katex@0.16.8': {} @@ -6074,9 +5608,9 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@25.9.3': + '@types/node@26.1.1': dependencies: - undici-types: 7.24.6 + undici-types: 8.3.0 '@types/pdfobject@2.2.5': {} @@ -6105,7 +5639,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.9.3 + '@types/node': 26.1.1 '@types/yargs-parser@21.0.3': {} @@ -6113,31 +5647,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/type-utils': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/utils': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.61.0 - eslint: 10.4.1(jiti@2.7.0) - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/type-utils': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.61.0 - eslint: 10.4.1(jiti@2.7.0) + '@typescript-eslint/parser': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.63.0 + '@typescript-eslint/type-utils': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.63.0 + eslint: 10.6.0(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -6145,142 +5663,79 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/parser@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.61.0 + '@typescript-eslint/scope-manager': 8.63.0 + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.63.0 debug: 4.4.3 - eslint: 10.4.1(jiti@2.7.0) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.61.0 - debug: 4.4.3 - eslint: 10.4.1(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.61.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.63.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.61.0(typescript@5.9.3) - '@typescript-eslint/types': 8.61.0 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.61.0(typescript@6.0.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.61.0(typescript@6.0.3) - '@typescript-eslint/types': 8.61.0 + '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@6.0.3) + '@typescript-eslint/types': 8.63.0 debug: 4.4.3 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.61.0': + '@typescript-eslint/scope-manager@8.63.0': dependencies: - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/visitor-keys': 8.61.0 + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/visitor-keys': 8.63.0 - '@typescript-eslint/tsconfig-utils@8.61.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@typescript-eslint/tsconfig-utils@8.61.0(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.63.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) debug: 4.4.3 - eslint: 10.4.1(jiti@2.7.0) - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/type-utils@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)': - dependencies: - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) - debug: 4.4.3 - eslint: 10.4.1(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.61.0': {} + '@typescript-eslint/types@8.63.0': {} - '@typescript-eslint/typescript-estree@8.61.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.63.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.61.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.61.0(typescript@5.9.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/visitor-keys': 8.61.0 + '@typescript-eslint/project-service': 8.63.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@6.0.3) + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/visitor-keys': 8.63.0 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.8.1 - tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/typescript-estree@8.61.0(typescript@6.0.3)': - dependencies: - '@typescript-eslint/project-service': 8.61.0(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.61.0(typescript@6.0.3) - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/visitor-keys': 8.61.0 - debug: 4.4.3 - minimatch: 10.2.5 - semver: 7.8.1 + semver: 7.8.4 tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/utils@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3) - eslint: 10.4.1(jiti@2.7.0) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/types': 8.61.0 - '@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3) - eslint: 10.4.1(jiti@2.7.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.63.0 + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.61.0': + '@typescript-eslint/visitor-keys@8.63.0': dependencies: - '@typescript-eslint/types': 8.61.0 + '@typescript-eslint/types': 8.63.0 eslint-visitor-keys: 5.0.1 '@unrs/resolver-binding-android-arm-eabi@1.12.2': @@ -6341,7 +5796,7 @@ snapshots: dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': @@ -6358,62 +5813,62 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) - '@vitejs/plugin-vue@6.0.7(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0))(vue@3.5.37(typescript@6.0.3))': + '@vitejs/plugin-vue@6.0.7(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39(typescript@6.0.3))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0) - vue: 3.5.37(typescript@6.0.3) + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) + vue: 3.5.39(typescript@6.0.3) - '@vitest/eslint-plugin@1.6.20(@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8(@types/node@25.9.3)(happy-dom@20.10.2)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)))': + '@vitest/eslint-plugin@1.6.22(@typescript-eslint/eslint-plugin@8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.10(@types/node@26.1.1)(happy-dom@20.10.6)(jsdom@20.0.3)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)))': dependencies: - '@typescript-eslint/scope-manager': 8.61.0 - '@typescript-eslint/utils': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.4.1(jiti@2.7.0) + '@typescript-eslint/scope-manager': 8.63.0 + '@typescript-eslint/utils': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) typescript: 6.0.3 - vitest: 4.1.8(@types/node@25.9.3)(happy-dom@20.10.2)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)) + vitest: 4.1.10(@types/node@26.1.1)(happy-dom@20.10.6)(jsdom@20.0.3)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) transitivePeerDependencies: - supports-color - '@vitest/expect@4.1.8': + '@vitest/expect@4.1.10': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.8 - '@vitest/utils': 4.1.8 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0))': + '@vitest/mocker@4.1.10(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))': dependencies: - '@vitest/spy': 4.1.8 + '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0) + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) - '@vitest/pretty-format@4.1.8': + '@vitest/pretty-format@4.1.10': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.8': + '@vitest/runner@4.1.10': dependencies: - '@vitest/utils': 4.1.8 + '@vitest/utils': 4.1.10 pathe: 2.0.3 - '@vitest/snapshot@4.1.8': + '@vitest/snapshot@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.8 - '@vitest/utils': 4.1.8 + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.8': {} + '@vitest/spy@4.1.10': {} - '@vitest/utils@4.1.8': + '@vitest/utils@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.8 + '@vitest/pretty-format': 4.1.10 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -6429,69 +5884,69 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.1.0 - '@vue/compiler-core@3.5.37': + '@vue/compiler-core@3.5.39': dependencies: '@babel/parser': 7.29.7 - '@vue/shared': 3.5.37 + '@vue/shared': 3.5.39 entities: 7.0.1 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.37': + '@vue/compiler-dom@3.5.39': dependencies: - '@vue/compiler-core': 3.5.37 - '@vue/shared': 3.5.37 + '@vue/compiler-core': 3.5.39 + '@vue/shared': 3.5.39 - '@vue/compiler-sfc@3.5.37': + '@vue/compiler-sfc@3.5.39': dependencies: '@babel/parser': 7.29.7 - '@vue/compiler-core': 3.5.37 - '@vue/compiler-dom': 3.5.37 - '@vue/compiler-ssr': 3.5.37 - '@vue/shared': 3.5.37 + '@vue/compiler-core': 3.5.39 + '@vue/compiler-dom': 3.5.39 + '@vue/compiler-ssr': 3.5.39 + '@vue/shared': 3.5.39 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.15 + postcss: 8.5.16 source-map-js: 1.2.1 - '@vue/compiler-ssr@3.5.37': + '@vue/compiler-ssr@3.5.39': dependencies: - '@vue/compiler-dom': 3.5.37 - '@vue/shared': 3.5.37 + '@vue/compiler-dom': 3.5.39 + '@vue/shared': 3.5.39 - '@vue/language-core@3.3.4': + '@vue/language-core@3.3.7': dependencies: '@volar/language-core': 2.4.28 - '@vue/compiler-dom': 3.5.37 - '@vue/shared': 3.5.37 + '@vue/compiler-dom': 3.5.39 + '@vue/shared': 3.5.39 alien-signals: 3.2.1 muggle-string: 0.4.1 path-browserify: 1.0.1 - picomatch: 4.0.4 + picomatch: 4.0.5 - '@vue/reactivity@3.5.37': + '@vue/reactivity@3.5.39': dependencies: - '@vue/shared': 3.5.37 + '@vue/shared': 3.5.39 - '@vue/runtime-core@3.5.37': + '@vue/runtime-core@3.5.39': dependencies: - '@vue/reactivity': 3.5.37 - '@vue/shared': 3.5.37 + '@vue/reactivity': 3.5.39 + '@vue/shared': 3.5.39 - '@vue/runtime-dom@3.5.37': + '@vue/runtime-dom@3.5.39': dependencies: - '@vue/reactivity': 3.5.37 - '@vue/runtime-core': 3.5.37 - '@vue/shared': 3.5.37 + '@vue/reactivity': 3.5.39 + '@vue/runtime-core': 3.5.39 + '@vue/shared': 3.5.39 csstype: 3.2.3 - '@vue/server-renderer@3.5.37(vue@3.5.37(typescript@6.0.3))': + '@vue/server-renderer@3.5.39(vue@3.5.39(typescript@6.0.3))': dependencies: - '@vue/compiler-ssr': 3.5.37 - '@vue/shared': 3.5.37 - vue: 3.5.37(typescript@6.0.3) + '@vue/compiler-ssr': 3.5.39 + '@vue/shared': 3.5.39 + vue: 3.5.39(typescript@6.0.3) - '@vue/shared@3.5.37': {} + '@vue/shared@3.5.39': {} abab@2.0.6: {} @@ -6532,16 +5987,10 @@ snapshots: alien-signals@3.2.1: {} - ansi-escapes@1.4.0: {} - - ansi-regex@2.1.1: {} - ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} - ansi-styles@2.2.1: {} - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -6561,12 +6010,11 @@ snapshots: argparse@2.0.1: {} - aria-query@5.3.2: {} - array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 is-array-buffer: 3.0.5 + optional: true array-includes@3.1.9: dependencies: @@ -6578,6 +6026,7 @@ snapshots: get-intrinsic: 1.3.0 is-string: 1.1.1 math-intrinsics: 1.1.0 + optional: true array.prototype.findlastindex@1.2.6: dependencies: @@ -6588,6 +6037,7 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.2 es-shim-unscopables: 1.1.0 + optional: true array.prototype.flat@1.3.3: dependencies: @@ -6595,6 +6045,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 es-shim-unscopables: 1.1.0 + optional: true array.prototype.flatmap@1.3.3: dependencies: @@ -6602,6 +6053,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 es-shim-unscopables: 1.1.0 + optional: true arraybuffer.prototype.slice@1.0.4: dependencies: @@ -6612,42 +6064,30 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + optional: true - asciinema-player@3.15.1: + asciinema-player@3.17.0: dependencies: '@babel/runtime': 7.29.7 solid-js: 1.9.13 solid-transition-group: 0.2.3(solid-js@1.9.13) - asn1@0.2.6: - dependencies: - safer-buffer: 2.1.2 - - assert-plus@1.0.0: {} - assertion-error@2.0.1: {} - ast-types-flow@0.0.8: {} - astral-regex@2.0.0: {} - async-function@1.0.0: {} + async-function@1.0.0: + optional: true asynckit@0.4.0: {} available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 + optional: true - aws-sign2@0.7.0: {} - - aws4@1.13.2: {} - - axe-core@4.12.0: {} - - axobject-query@4.1.0: {} - - balanced-match@1.0.2: {} + balanced-match@1.0.2: + optional: true balanced-match@4.0.4: {} @@ -6655,32 +6095,17 @@ snapshots: baseline-browser-mapping@2.10.33: {} - bcrypt-pbkdf@1.0.2: - dependencies: - tweetnacl: 0.14.5 + baseline-browser-mapping@2.10.43: {} binary-extensions@2.3.0: {} - biome@0.3.3: - dependencies: - bluebird: 3.7.2 - chalk: 1.1.3 - commander: 2.20.3 - editor: 1.0.0 - fs-promise: 0.5.0 - inquirer-promise: 0.0.3 - request-promise: 3.0.0 - untildify: 3.0.3 - user-home: 2.0.0 - - bluebird@3.7.2: {} - boolbase@1.0.0: {} brace-expansion@1.1.15: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 + optional: true brace-expansion@5.0.6: dependencies: @@ -6698,9 +6123,17 @@ snapshots: node-releases: 2.0.46 update-browserslist-db: 1.2.3(browserslist@4.28.2) + browserslist@4.28.6: + dependencies: + baseline-browser-mapping: 2.10.43 + caniuse-lite: 1.0.30001805 + electron-to-chromium: 1.5.389 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.6) + buffer-image-size@0.6.4: dependencies: - '@types/node': 25.9.3 + '@types/node': 26.1.1 buffer@5.7.1: dependencies: @@ -6732,11 +6165,13 @@ snapshots: es-define-property: 1.0.1 get-intrinsic: 1.3.0 set-function-length: 1.2.2 + optional: true call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + optional: true callsites@3.1.0: {} @@ -6744,18 +6179,10 @@ snapshots: caniuse-lite@1.0.30001793: {} - caseless@0.12.0: {} + caniuse-lite@1.0.30001805: {} chai@6.2.2: {} - chalk@1.1.3: - dependencies: - ansi-styles: 2.2.1 - escape-string-regexp: 1.0.5 - has-ansi: 2.0.0 - strip-ansi: 3.0.1 - supports-color: 2.0.0 - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -6804,23 +6231,11 @@ snapshots: citeproc@2.4.63: {} - clean-regexp@1.0.0: - dependencies: - escape-string-regexp: 1.0.5 - - cli-cursor@1.0.2: - dependencies: - restore-cursor: 1.0.1 - - cli-width@1.1.1: {} - - clippie@4.2.0: {} - - code-point-at@1.1.0: {} + clippie@4.2.1: {} codemirror-lang-elixir@4.0.1: dependencies: - '@codemirror/language': 6.12.3 + '@codemirror/language': 6.12.4 lezer-elixir: 1.1.3 codemirror-spell-checker@1.1.2: @@ -6843,9 +6258,7 @@ snapshots: commander@11.1.0: {} - commander@14.0.3: {} - - commander@2.20.3: {} + commander@15.0.0: {} commander@4.1.1: {} @@ -6857,7 +6270,10 @@ snapshots: compare-versions@6.1.1: {} - concat-map@0.0.1: {} + concat-map@0.0.1: + optional: true + + convert-hrtime@5.0.0: {} convert-source-map@2.0.0: {} @@ -6865,12 +6281,8 @@ snapshots: dependencies: browserslist: 4.28.2 - core-js@2.6.12: {} - core-js@3.32.2: {} - core-util-is@1.0.2: {} - cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -6879,7 +6291,7 @@ snapshots: dependencies: layout-base: 2.0.1 - cosmiconfig@9.0.1(typescript@6.0.3): + cosmiconfig@9.0.2(typescript@6.0.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 @@ -7120,12 +6532,6 @@ snapshots: d3: 7.9.0 lodash-es: 4.18.1 - damerau-levenshtein@1.0.8: {} - - dashdash@1.14.1: - dependencies: - assert-plus: 1.0.0 - data-urls@3.0.2: dependencies: abab: 2.0.6 @@ -7137,24 +6543,28 @@ snapshots: call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 + optional: true data-view-byte-length@1.0.2: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 + optional: true data-view-byte-offset@1.0.1: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 + optional: true dayjs@1.11.21: {} debug@3.2.7: dependencies: ms: 2.1.3 + optional: true debug@4.4.3: dependencies: @@ -7170,22 +6580,19 @@ snapshots: deep-is@0.1.4: {} - deep-rename-keys@0.2.1: - dependencies: - kind-of: 3.2.2 - rename-keys: 1.2.0 - define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 es-errors: 1.3.0 gopd: 1.2.0 + optional: true define-properties@1.2.1: dependencies: define-data-property: 1.1.4 has-property-descriptors: 1.0.2 object-keys: 1.1.1 + optional: true delaunator@5.1.0: dependencies: @@ -7195,6 +6602,8 @@ snapshots: dequal@2.0.3: {} + detect-indent@7.0.2: {} + detect-libc@2.1.2: {} devlop@1.1.0: @@ -7208,6 +6617,7 @@ snapshots: doctrine@2.1.0: dependencies: esutils: 2.0.3 + optional: true dom-input-range@2.0.1: {} @@ -7243,13 +6653,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - earlgrey-runtime@0.1.2: - dependencies: - core-js: 2.6.12 - kaiser: 0.0.4 - lodash: 4.18.1 - regenerator-runtime: 0.9.6 - easymde@2.21.0: dependencies: '@types/codemirror': 5.60.17 @@ -7258,21 +6661,14 @@ snapshots: codemirror-spell-checker: 1.1.2 marked: 4.3.0 - ecc-jsbn@0.1.2: - dependencies: - jsbn: 0.1.1 - safer-buffer: 2.1.2 - - editor@1.0.0: {} - electron-to-chromium@1.5.364: {} + electron-to-chromium@1.5.389: {} + elkjs@0.9.3: {} emoji-regex@8.0.0: {} - emoji-regex@9.2.2: {} - entities@4.5.0: {} entities@6.0.1: {} @@ -7341,6 +6737,7 @@ snapshots: typed-array-length: 1.0.8 unbox-primitive: 1.1.0 which-typed-array: 1.1.21 + optional: true es-define-property@1.0.1: {} @@ -7362,12 +6759,14 @@ snapshots: es-shim-unscopables@1.1.0: dependencies: hasown: 2.0.4 + optional: true es-to-primitive@1.3.0: dependencies: is-callable: 1.2.7 is-date-object: 1.1.0 is-symbol: 1.1.1 + optional: true es-toolkit@1.47.0: {} @@ -7402,8 +6801,6 @@ snapshots: escalade@3.2.0: {} - escape-string-regexp@1.0.5: {} - escape-string-regexp@2.0.0: {} escape-string-regexp@4.0.0: {} @@ -7416,10 +6813,6 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.7.0)): - dependencies: - eslint: 10.4.1(jiti@2.7.0) - eslint-import-context@0.1.9(unrs-resolver@1.12.2): dependencies: get-tsconfig: 4.14.0 @@ -7434,11 +6827,12 @@ snapshots: resolve: 2.0.0-next.7 transitivePeerDependencies: - supports-color + optional: true - eslint-import-resolver-typescript@4.4.5(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.1(jiti@2.7.0)): + eslint-import-resolver-typescript@4.4.5(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.6.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.6.0(jiti@2.7.0)): dependencies: debug: 4.4.3 - eslint: 10.4.1(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) eslint-import-context: 0.1.9(unrs-resolver@1.12.2) get-tsconfig: 4.14.0 is-bun-module: 2.0.0 @@ -7446,117 +6840,42 @@ snapshots: tinyglobby: 0.2.17 unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)) - eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.6.0(jiti@2.7.0)) + eslint-plugin-import-x: 4.17.1(@typescript-eslint/utils@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.6.0(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.13.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)): + eslint-module-utils@2.13.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.5)(eslint@10.6.0(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) - eslint: 10.4.1(jiti@2.7.0) + '@typescript-eslint/parser': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 4.4.5(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.1(jiti@2.7.0)) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.13.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.4.1(jiti@2.7.0) - eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 4.4.5(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.1(jiti@2.7.0)) + eslint-import-resolver-typescript: 4.4.5(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.6.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.6.0(jiti@2.7.0)) transitivePeerDependencies: - supports-color optional: true - eslint-plugin-array-func@5.1.1(eslint@10.4.1(jiti@2.7.0)): + eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.6.0(jiti@2.7.0)): dependencies: - eslint: 10.4.1(jiti@2.7.0) - - eslint-plugin-de-morgan@2.1.2(eslint@10.4.1(jiti@2.7.0)): - dependencies: - eslint: 10.4.1(jiti@2.7.0) - - eslint-plugin-escompat@3.11.4(eslint@10.4.1(jiti@2.7.0)): - dependencies: - browserslist: 4.28.2 - eslint: 10.4.1(jiti@2.7.0) - - eslint-plugin-eslint-comments@3.2.0(eslint@10.4.1(jiti@2.7.0)): - dependencies: - escape-string-regexp: 1.0.5 - eslint: 10.4.1(jiti@2.7.0) - ignore: 5.3.2 - - eslint-plugin-filenames@1.3.2(eslint@10.4.1(jiti@2.7.0)): - dependencies: - eslint: 10.4.1(jiti@2.7.0) - lodash.camelcase: 4.3.0 - lodash.kebabcase: 4.1.1 - lodash.snakecase: 4.1.1 - lodash.upperfirst: 4.3.1 - - eslint-plugin-github@6.0.0(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)): - dependencies: - '@eslint/compat': 1.4.1(eslint@10.4.1(jiti@2.7.0)) - '@eslint/eslintrc': 3.3.5 - '@eslint/js': 9.39.4 - '@github/browserslist-config': 1.0.0 - '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/parser': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) - aria-query: 5.3.2 - eslint: 10.4.1(jiti@2.7.0) - eslint-config-prettier: 10.1.8(eslint@10.4.1(jiti@2.7.0)) - eslint-plugin-escompat: 3.11.4(eslint@10.4.1(jiti@2.7.0)) - eslint-plugin-eslint-comments: 3.2.0(eslint@10.4.1(jiti@2.7.0)) - eslint-plugin-filenames: 1.3.2(eslint@10.4.1(jiti@2.7.0)) - eslint-plugin-i18n-text: 1.0.1(eslint@10.4.1(jiti@2.7.0)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@10.4.1(jiti@2.7.0)) - eslint-plugin-no-only-tests: 3.4.0 - eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.7.0)))(eslint@10.4.1(jiti@2.7.0))(prettier@3.8.3) - eslint-rule-documentation: 1.0.23 - globals: 16.5.0 - jsx-ast-utils: 3.3.5 - prettier: 3.8.3 - svg-element-attributes: 1.3.1 - typescript: 5.9.3 - typescript-eslint: 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) - transitivePeerDependencies: - - '@types/eslint' - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-i18n-text@1.0.1(eslint@10.4.1(jiti@2.7.0)): - dependencies: - eslint: 10.4.1(jiti@2.7.0) - - eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)): - dependencies: - '@package-json/types': 0.0.12 - '@typescript-eslint/types': 8.61.0 + '@typescript-eslint/types': 8.63.0 comment-parser: 1.4.7 debug: 4.4.3 - eslint: 10.4.1(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) eslint-import-context: 0.1.9(unrs-resolver@1.12.2) is-glob: 4.0.3 minimatch: 10.2.5 - semver: 7.8.1 + semver: 7.8.4 stable-hash-x: 0.2.0 unrs-resolver: 1.12.2 optionalDependencies: - '@typescript-eslint/utils': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) eslint-import-resolver-node: 0.3.10 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.6.0(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -7565,9 +6884,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 10.4.1(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)) + eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.5)(eslint@10.6.0(jiti@2.7.0)) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -7579,156 +6898,97 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 10.4.1(jiti@2.7.0) - eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)) - hasown: 2.0.4 - is-core-module: 2.16.2 - is-glob: 4.0.3 - minimatch: 3.1.5 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color optional: true - eslint-plugin-jsx-a11y@6.10.2(eslint@10.4.1(jiti@2.7.0)): + eslint-plugin-playwright@2.10.5(eslint@10.6.0(jiti@2.7.0)): dependencies: - aria-query: 5.3.2 - array-includes: 3.1.9 - array.prototype.flatmap: 1.3.3 - ast-types-flow: 0.0.8 - axe-core: 4.12.0 - axobject-query: 4.1.0 - damerau-levenshtein: 1.0.8 - emoji-regex: 9.2.2 - eslint: 10.4.1(jiti@2.7.0) - hasown: 2.0.4 - jsx-ast-utils: 3.3.5 - language-tags: 1.0.9 - minimatch: 3.1.5 - object.fromentries: 2.0.8 - safe-regex-test: 1.1.0 - string.prototype.includes: 2.0.1 + eslint: 10.6.0(jiti@2.7.0) + globals: 17.7.0 - eslint-plugin-no-only-tests@3.4.0: {} - - eslint-plugin-playwright@2.10.4(eslint@10.4.1(jiti@2.7.0)): + eslint-plugin-regexp@3.1.1(eslint@10.6.0(jiti@2.7.0)): dependencies: - eslint: 10.4.1(jiti@2.7.0) - globals: 17.6.0 - - eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.7.0)))(eslint@10.4.1(jiti@2.7.0))(prettier@3.8.3): - dependencies: - eslint: 10.4.1(jiti@2.7.0) - prettier: 3.8.3 - prettier-linter-helpers: 1.0.1 - synckit: 0.11.13 - optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@10.4.1(jiti@2.7.0)) - - eslint-plugin-regexp@3.1.0(eslint@10.4.1(jiti@2.7.0)): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 comment-parser: 1.4.7 - eslint: 10.4.1(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) jsdoc-type-pratt-parser: 7.2.0 refa: 0.12.1 regexp-ast-analysis: 0.7.1 scslre: 0.3.0 - eslint-plugin-sonarjs@4.0.3(eslint@10.4.1(jiti@2.7.0)): + eslint-plugin-sonarjs@4.1.0(eslint@10.6.0(jiti@2.7.0)): dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 10.4.1(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) functional-red-black-tree: 1.0.1 - globals: 17.6.0 + globals: 17.7.0 jsx-ast-utils-x: 0.1.0 lodash.merge: 4.6.2 minimatch: 10.2.5 scslre: 0.3.0 - semver: 7.8.1 + semver: 7.8.4 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 + yaml: 2.9.0 - eslint-plugin-unicorn@64.0.0(eslint@10.4.1(jiti@2.7.0)): + eslint-plugin-unicorn@71.1.0(eslint@10.6.0(jiti@2.7.0)): dependencies: - '@babel/helper-validator-identifier': 7.29.7 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) + browserslist: 4.28.6 change-case: 5.4.4 ci-info: 4.4.0 - clean-regexp: 1.0.0 core-js-compat: 3.49.0 - eslint: 10.4.1(jiti@2.7.0) + detect-indent: 7.0.2 + eslint: 10.6.0(jiti@2.7.0) find-up-simple: 1.0.1 - globals: 17.6.0 + globals: 17.7.0 indent-string: 5.0.0 is-builtin-module: 5.0.0 - jsesc: 3.1.0 + is-identifier: 1.1.0 pluralize: 8.0.0 - regexp-tree: 0.1.27 - regjsparser: 0.13.1 - semver: 7.8.1 + quote-js-string: 0.1.0 + regjsparser: 0.13.2 + reserved-identifiers: 1.2.0 + semver: 7.8.5 strip-indent: 4.1.1 - eslint-plugin-vue-scoped-css@3.1.1(eslint@10.4.1(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.1(jiti@2.7.0))): + eslint-plugin-vue-scoped-css@3.1.1(eslint@10.6.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.6.0(jiti@2.7.0))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) es-toolkit: 1.47.0 - eslint: 10.4.1(jiti@2.7.0) - postcss: 8.5.15 - postcss-safe-parser: 7.0.1(postcss@8.5.15) - postcss-selector-parser: 7.1.1 - vue-eslint-parser: 10.4.0(eslint@10.4.1(jiti@2.7.0)) + eslint: 10.6.0(jiti@2.7.0) + postcss: 8.5.16 + postcss-safe-parser: 7.0.1(postcss@8.5.16) + postcss-selector-parser: 7.1.4 + vue-eslint-parser: 10.4.0(eslint@10.6.0(jiti@2.7.0)) - eslint-plugin-vue@10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@10.4.1(jiti@2.7.0)))(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.1(jiti@2.7.0))): + eslint-plugin-vue@10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@10.6.0(jiti@2.7.0)))(@typescript-eslint/parser@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.6.0(jiti@2.7.0))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) - eslint: 10.4.1(jiti@2.7.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) + eslint: 10.6.0(jiti@2.7.0) natural-compare: 1.4.0 nth-check: 2.1.1 - postcss-selector-parser: 7.1.1 - semver: 7.8.1 - vue-eslint-parser: 10.4.0(eslint@10.4.1(jiti@2.7.0)) + postcss-selector-parser: 7.1.4 + semver: 7.8.4 + vue-eslint-parser: 10.4.0(eslint@10.6.0(jiti@2.7.0)) xml-name-validator: 4.0.0 optionalDependencies: - '@stylistic/eslint-plugin': 5.10.0(eslint@10.4.1(jiti@2.7.0)) - '@typescript-eslint/parser': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@stylistic/eslint-plugin': 5.10.0(eslint@10.6.0(jiti@2.7.0)) + '@typescript-eslint/parser': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) - eslint-plugin-wc@3.1.0(eslint@10.4.1(jiti@2.7.0)): + eslint-plugin-wc@3.1.0(eslint@10.6.0(jiti@2.7.0)): dependencies: - eslint: 10.4.1(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) is-valid-element-name: 1.0.0 js-levenshtein-esm: 2.0.0 - eslint-rule-documentation@1.0.23: {} - eslint-scope@9.1.2: dependencies: '@types/esrecurse': 4.3.1 @@ -7742,9 +7002,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.4.1(jiti@2.7.0): + eslint@10.6.0(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.6.0 @@ -7811,22 +7071,10 @@ snapshots: esutils@2.0.3: {} - eventemitter3@2.0.3: {} - - events@3.3.0: {} - - exit-hook@1.1.1: {} - expect-type@1.3.0: {} - extend@3.0.2: {} - - extsprintf@1.3.0: {} - fast-deep-equal@3.1.3: {} - fast-diff@1.3.0: {} - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7847,9 +7095,9 @@ snapshots: dependencies: reusify: 1.1.0 - fdir@6.5.0(picomatch@4.0.4): + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: - picomatch: 4.0.4 + picomatch: 4.0.5 fetch-ponyfill@7.1.0: dependencies: @@ -7859,11 +7107,6 @@ snapshots: fflate@0.8.2: {} - figures@1.7.0: - dependencies: - escape-string-regexp: 1.0.5 - object-assign: 4.1.1 - file-entry-cache@11.1.3: dependencies: flat-cache: 6.1.22 @@ -7899,14 +7142,7 @@ snapshots: for-each@0.3.5: dependencies: is-callable: 1.2.7 - - forever-agent@0.6.1: {} - - form-data@2.3.3: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 + optional: true form-data@4.0.5: dependencies: @@ -7916,23 +7152,6 @@ snapshots: hasown: 2.0.4 mime-types: 2.1.35 - fs-extra@0.26.7: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 2.4.0 - klaw: 1.3.1 - path-is-absolute: 1.0.1 - rimraf: 2.7.1 - - fs-promise@0.5.0: - dependencies: - any-promise: 1.3.0 - fs-extra: 0.26.7 - mz: 2.7.0 - thenify-all: 1.6.0 - - fs.realpath@1.0.0: {} - fsevents@2.3.2: optional: true @@ -7941,6 +7160,8 @@ snapshots: function-bind@1.1.2: {} + function-timeout@1.0.2: {} + function.prototype.name@1.1.8: dependencies: call-bind: 1.0.9 @@ -7949,12 +7170,15 @@ snapshots: functions-have-names: 1.2.3 hasown: 2.0.4 is-callable: 1.2.7 + optional: true functional-red-black-tree@1.0.1: {} - functions-have-names@1.2.3: {} + functions-have-names@1.2.3: + optional: true - generator-function@2.0.1: {} + generator-function@2.0.1: + optional: true get-east-asian-width@1.6.0: {} @@ -7981,15 +7205,12 @@ snapshots: call-bound: 1.0.4 es-errors: 1.3.0 get-intrinsic: 1.3.0 + optional: true get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 - getpass@0.1.7: - dependencies: - assert-plus: 1.0.0 - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -7998,15 +7219,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.5 - once: 1.4.0 - path-is-absolute: 1.0.1 - global-modules@2.0.0: dependencies: global-prefix: 3.0.0 @@ -8017,16 +7229,13 @@ snapshots: kind-of: 6.0.3 which: 1.3.1 - globals@14.0.0: {} - - globals@16.5.0: {} - - globals@17.6.0: {} + globals@17.7.0: {} globalthis@1.0.4: dependencies: define-properties: 1.2.1 gopd: 1.2.0 + optional: true globby@16.2.0: dependencies: @@ -8047,9 +7256,9 @@ snapshots: hammerjs@2.0.8: {} - happy-dom@20.10.2: + happy-dom@20.10.6: dependencies: - '@types/node': 25.9.3 + '@types/node': 26.1.1 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 buffer-image-size: 0.6.4 @@ -8060,18 +7269,8 @@ snapshots: - bufferutil - utf-8-validate - har-schema@2.0.0: {} - - har-validator@5.1.5: - dependencies: - ajv: 6.15.0 - har-schema: 2.0.0 - - has-ansi@2.0.0: - dependencies: - ansi-regex: 2.1.1 - - has-bigints@1.1.0: {} + has-bigints@1.1.0: + optional: true has-flag@4.0.0: {} @@ -8080,10 +7279,12 @@ snapshots: has-property-descriptors@1.0.2: dependencies: es-define-property: 1.0.1 + optional: true has-proto@1.2.0: dependencies: dunder-proto: 1.0.1 + optional: true has-symbols@1.1.0: {} @@ -8124,12 +7325,6 @@ snapshots: transitivePeerDependencies: - supports-color - http-signature@1.2.0: - dependencies: - assert-plus: 1.0.0 - jsprim: 1.4.2 - sshpk: 1.18.0 - https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -8141,6 +7336,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + identifier-regex@1.1.0: + dependencies: + reserved-identifiers: 1.2.0 + idiomorph@0.7.4: {} ieee754@1.2.1: {} @@ -8160,43 +7359,16 @@ snapshots: indent-string@5.0.0: {} - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - ini@1.3.8: {} ini@4.1.3: {} - inquirer-promise@0.0.3: - dependencies: - earlgrey-runtime: 0.1.2 - inquirer: 0.11.4 - - inquirer@0.11.4: - dependencies: - ansi-escapes: 1.4.0 - ansi-regex: 2.1.1 - chalk: 1.1.3 - cli-cursor: 1.0.2 - cli-width: 1.1.1 - figures: 1.7.0 - lodash: 3.10.1 - readline2: 1.0.1 - run-async: 0.1.0 - rx-lite: 3.1.2 - string-width: 1.0.2 - strip-ansi: 3.0.1 - through: 2.3.8 - internal-slot@1.1.0: dependencies: es-errors: 1.3.0 hasown: 2.0.4 side-channel: 1.1.0 + optional: true internmap@1.0.1: {} @@ -8214,6 +7386,7 @@ snapshots: call-bind: 1.0.9 call-bound: 1.0.4 get-intrinsic: 1.3.0 + optional: true is-arrayish@0.2.1: {} @@ -8224,10 +7397,12 @@ snapshots: get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 + optional: true is-bigint@1.1.0: dependencies: has-bigints: 1.1.0 + optional: true is-binary-path@2.1.0: dependencies: @@ -8237,8 +7412,7 @@ snapshots: dependencies: call-bound: 1.0.4 has-tostringtag: 1.0.2 - - is-buffer@1.1.6: {} + optional: true is-builtin-module@5.0.0: dependencies: @@ -8246,9 +7420,10 @@ snapshots: is-bun-module@2.0.0: dependencies: - semver: 7.8.1 + semver: 7.8.4 - is-callable@1.2.7: {} + is-callable@1.2.7: + optional: true is-core-module@2.16.2: dependencies: @@ -8259,11 +7434,13 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 is-typed-array: 1.1.15 + optional: true is-date-object@1.1.0: dependencies: call-bound: 1.0.4 has-tostringtag: 1.0.2 + optional: true is-decimal@2.0.1: {} @@ -8272,10 +7449,7 @@ snapshots: is-finalizationregistry@1.1.1: dependencies: call-bound: 1.0.4 - - is-fullwidth-code-point@1.0.0: - dependencies: - number-is-nan: 1.0.1 + optional: true is-fullwidth-code-point@3.0.0: {} @@ -8286,6 +7460,7 @@ snapshots: get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 + optional: true is-glob@4.0.3: dependencies: @@ -8293,14 +7468,22 @@ snapshots: is-hexadecimal@2.0.1: {} - is-map@2.0.3: {} + is-identifier@1.1.0: + dependencies: + identifier-regex: 1.1.0 + super-regex: 1.1.0 - is-negative-zero@2.0.3: {} + is-map@2.0.3: + optional: true + + is-negative-zero@2.0.3: + optional: true is-number-object@1.1.1: dependencies: call-bound: 1.0.4 has-tostringtag: 1.0.2 + optional: true is-number@7.0.0: {} @@ -8314,58 +7497,64 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 hasown: 2.0.4 + optional: true - is-set@2.0.3: {} + is-set@2.0.3: + optional: true is-shared-array-buffer@1.0.4: dependencies: call-bound: 1.0.4 + optional: true is-string@1.1.1: dependencies: call-bound: 1.0.4 has-tostringtag: 1.0.2 + optional: true is-symbol@1.1.1: dependencies: call-bound: 1.0.4 has-symbols: 1.1.0 safe-regex-test: 1.1.0 + optional: true is-typed-array@1.1.15: dependencies: which-typed-array: 1.1.21 - - is-typedarray@1.0.0: {} + optional: true is-valid-element-name@1.0.0: dependencies: is-potential-custom-element-name: 1.0.1 - is-weakmap@2.0.2: {} + is-weakmap@2.0.2: + optional: true is-weakref@1.1.1: dependencies: call-bound: 1.0.4 + optional: true is-weakset@2.0.4: dependencies: call-bound: 1.0.4 get-intrinsic: 1.3.0 + optional: true - isarray@2.0.5: {} + isarray@2.0.5: + optional: true isexe@2.0.0: {} - isstream@0.1.2: {} - jest-environment-jsdom@29.7.0: dependencies: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 '@types/jsdom': 20.0.1 - '@types/node': 25.9.3 + '@types/node': 26.1.1 jest-mock: 29.7.0 jest-util: 29.7.0 jsdom: 20.0.3 @@ -8389,13 +7578,13 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 25.9.3 + '@types/node': 26.1.1 jest-util: 29.7.0 jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 25.9.3 + '@types/node': 26.1.1 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -8413,15 +7602,13 @@ snapshots: js-tokens@9.0.1: {} - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - js-yaml@4.2.0: dependencies: argparse: 2.0.1 - jsbn@0.1.1: {} + js-yaml@5.2.1: + dependencies: + argparse: 2.0.1 jsdoc-type-pratt-parser@7.2.0: {} @@ -8468,44 +7655,19 @@ snapshots: json-schema-traverse@1.0.0: {} - json-schema@0.4.0: {} - json-stable-stringify-without-jsonify@1.0.1: {} - json-stringify-safe@5.0.1: {} - json5@1.0.2: dependencies: minimist: 1.2.8 + optional: true jsonc-parser@3.3.1: {} - jsonfile@2.4.0: - optionalDependencies: - graceful-fs: 4.2.11 - jsonpointer@5.0.1: {} - jsprim@1.4.2: - dependencies: - assert-plus: 1.0.0 - extsprintf: 1.3.0 - json-schema: 0.4.0 - verror: 1.10.0 - jsx-ast-utils-x@0.1.0: {} - jsx-ast-utils@3.3.5: - dependencies: - array-includes: 3.1.9 - array.prototype.flat: 1.3.3 - object.assign: 4.1.7 - object.values: 1.2.1 - - kaiser@0.0.4: - dependencies: - earlgrey-runtime: 0.1.2 - katex@0.16.47: dependencies: commander: 8.3.0 @@ -8524,22 +7686,8 @@ snapshots: khroma@2.1.0: {} - kind-of@3.2.2: - dependencies: - is-buffer: 1.1.6 - kind-of@6.0.3: {} - klaw@1.3.1: - optionalDependencies: - graceful-fs: 4.2.11 - - language-subtag-registry@0.3.23: {} - - language-tags@1.0.9: - dependencies: - language-subtag-registry: 0.3.23 - layout-base@1.0.2: {} layout-base@2.0.1: {} @@ -8617,27 +7765,21 @@ snapshots: lodash-es@4.18.1: {} - lodash.camelcase@4.3.0: {} - - lodash.kebabcase@4.1.1: {} - lodash.merge@4.6.2: {} - lodash.snakecase@4.1.1: {} - lodash.truncate@4.4.2: {} - lodash.upperfirst@4.3.1: {} - - lodash@3.10.1: {} - - lodash@4.18.1: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - markdown-it@14.1.1: + make-asynchronous@1.1.0: + dependencies: + p-event: 6.0.1 + type-fest: 4.41.0 + web-worker: 1.5.0 + + markdown-it@14.2.0: dependencies: argparse: 2.0.1 entities: 4.5.0 @@ -8646,16 +7788,16 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 - markdownlint-cli@0.48.0: + markdownlint-cli@0.49.0: dependencies: - commander: 14.0.3 + commander: 15.0.0 deep-extend: 0.6.0 ignore: 7.0.5 - js-yaml: 4.1.1 + js-yaml: 4.2.0 jsonc-parser: 3.3.1 jsonpointer: 5.0.1 - markdown-it: 14.1.1 - markdownlint: 0.40.0 + markdown-it: 14.2.0 + markdownlint: 0.41.0 minimatch: 10.2.5 run-con: 1.3.2 smol-toml: 1.6.1 @@ -8663,7 +7805,7 @@ snapshots: transitivePeerDependencies: - supports-color - markdownlint@0.40.0: + markdownlint@0.41.0: dependencies: micromark: 4.0.2 micromark-core-commonmark: 2.0.3 @@ -8673,7 +7815,7 @@ snapshots: micromark-extension-gfm-table: 2.1.1 micromark-extension-math: 3.1.0 micromark-util-types: 2.0.2 - string-width: 8.1.0 + string-width: 8.2.1 transitivePeerDependencies: - supports-color @@ -8681,13 +7823,10 @@ snapshots: marked@4.3.0: {} - material-icon-theme@5.35.0: + material-icon-theme@5.36.1: dependencies: - biome: 0.3.3 chroma-js: 3.2.0 - events: 3.3.0 fast-deep-equal: 3.1.3 - svgson: 5.3.1 math-intrinsics@1.1.0: {} @@ -8703,11 +7842,11 @@ snapshots: merge2@1.4.1: {} - mermaid@11.15.0: + mermaid@11.16.0: dependencies: '@braintree/sanitize-url': 7.1.2 '@iconify/utils': 3.1.3 - '@mermaid-js/parser': 1.1.1 + '@mermaid-js/parser': 1.2.0 '@types/d3': 7.4.3 '@upsetjs/venn.js': 2.0.0 cytoscape: 3.33.4 @@ -8917,6 +8056,7 @@ snapshots: minimatch@3.1.5: dependencies: brace-expansion: 1.1.15 + optional: true minimist@1.2.8: {} @@ -8926,8 +8066,6 @@ snapshots: muggle-string@0.4.1: {} - mute-stream@0.0.5: {} - mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -8948,6 +8086,7 @@ snapshots: es-errors: 1.3.0 object.entries: 1.1.9 semver: 6.3.1 + optional: true node-fetch@2.6.13: dependencies: @@ -8959,25 +8098,25 @@ snapshots: node-releases@2.0.46: {} + node-releases@2.0.51: {} + normalize-path@3.0.0: {} nth-check@2.1.1: dependencies: boolbase: 1.0.0 - number-is-nan@1.0.1: {} - nwsapi@2.2.23: {} - oauth-sign@0.9.0: {} - object-assign@4.1.1: {} object-hash@3.0.0: {} - object-inspect@1.13.4: {} + object-inspect@1.13.4: + optional: true - object-keys@1.1.1: {} + object-keys@1.1.1: + optional: true object.assign@4.1.7: dependencies: @@ -8987,6 +8126,7 @@ snapshots: es-object-atoms: 1.1.2 has-symbols: 1.1.0 object-keys: 1.1.1 + optional: true object.entries@1.1.9: dependencies: @@ -8994,6 +8134,7 @@ snapshots: call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.2 + optional: true object.fromentries@2.0.8: dependencies: @@ -9001,12 +8142,14 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 es-object-atoms: 1.1.2 + optional: true object.groupby@1.0.3: dependencies: call-bind: 1.0.9 define-properties: 1.2.1 es-abstract: 1.24.2 + optional: true object.values@1.2.1: dependencies: @@ -9014,15 +8157,10 @@ snapshots: call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.2 + optional: true obug@2.1.1: {} - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - onetime@1.1.0: {} - online-3d-viewer@0.18.0: dependencies: '@simonwep/pickr': 1.9.0 @@ -9038,13 +8176,16 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - os-homedir@1.0.2: {} - own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 object-keys: 1.1.1 safe-push-apply: 1.0.0 + optional: true + + p-event@6.0.1: + dependencies: + p-timeout: 6.1.4 p-limit@3.1.0: dependencies: @@ -9054,6 +8195,8 @@ snapshots: dependencies: p-limit: 3.1.0 + p-timeout@6.1.4: {} + package-manager-detector@1.6.0: {} parent-module@1.0.1: @@ -9087,8 +8230,6 @@ snapshots: path-exists@4.0.0: {} - path-is-absolute@1.0.1: {} - path-key@3.1.1: {} path-parse@1.0.7: {} @@ -9099,23 +8240,21 @@ snapshots: perfect-debounce@2.1.0: {} - performance-now@2.1.0: {} - picocolors@1.1.1: {} picomatch@2.3.2: {} - picomatch@4.0.4: {} + picomatch@4.0.5: {} pify@2.3.0: {} pirates@4.0.7: {} - playwright-core@1.60.0: {} + playwright-core@1.61.1: {} - playwright@1.60.0: + playwright@1.61.1: dependencies: - playwright-core: 1.60.0 + playwright-core: 1.61.1 optionalDependencies: fsevents: 2.3.2 @@ -9128,60 +8267,62 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 - possible-typed-array-names@1.1.0: {} + possible-typed-array-names@1.1.0: + optional: true postcss-html@1.8.1: dependencies: htmlparser2: 8.0.2 js-tokens: 9.0.1 - postcss: 8.5.15 - postcss-safe-parser: 6.0.0(postcss@8.5.15) + postcss: 8.5.16 + postcss-safe-parser: 6.0.0(postcss@8.5.16) - postcss-import@15.1.0(postcss@8.5.15): + postcss-import@15.1.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.12 - postcss-js@4.1.0(postcss@8.5.15): + postcss-js@4.1.0(postcss@8.5.16): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.15 + postcss: 8.5.16 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.15): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.16)(yaml@2.9.0): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 - postcss: 8.5.15 + postcss: 8.5.16 + yaml: 2.9.0 - postcss-nested@6.2.0(postcss@8.5.15): + postcss-nested@6.2.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 6.1.2 - postcss-safe-parser@6.0.0(postcss@8.5.15): + postcss-safe-parser@6.0.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 - postcss-safe-parser@7.0.1(postcss@8.5.15): + postcss-safe-parser@7.0.1(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@7.1.1: + postcss-selector-parser@7.1.4: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 postcss-value-parser@4.2.0: {} - postcss@8.5.15: + postcss@8.5.16: dependencies: nanoid: 3.3.12 picocolors: 1.1.1 @@ -9189,12 +8330,6 @@ snapshots: prelude-ls@1.2.1: {} - prettier-linter-helpers@1.0.1: - dependencies: - fast-diff: 1.3.0 - - prettier@3.8.3: {} - pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 @@ -9213,12 +8348,12 @@ snapshots: dependencies: hookified: 2.2.0 - qs@6.5.5: {} - querystringify@2.2.0: {} queue-microtask@1.2.3: {} + quote-js-string@0.1.0: {} + react-is@18.3.1: {} read-cache@1.0.0: @@ -9229,12 +8364,6 @@ snapshots: dependencies: picomatch: 2.3.2 - readline2@1.0.1: - dependencies: - code-point-at: 1.1.0 - is-fullwidth-code-point: 1.0.0 - mute-stream: 0.0.5 - refa@0.12.1: dependencies: '@eslint-community/regexpp': 4.12.2 @@ -9249,16 +8378,13 @@ snapshots: get-intrinsic: 1.3.0 get-proto: 1.0.1 which-builtin-type: 1.2.1 - - regenerator-runtime@0.9.6: {} + optional: true regexp-ast-analysis@0.7.1: dependencies: '@eslint-community/regexpp': 4.12.2 refa: 0.12.1 - regexp-tree@0.1.27: {} - regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.9 @@ -9267,46 +8393,18 @@ snapshots: get-proto: 1.0.1 gopd: 1.2.0 set-function-name: 2.0.2 + optional: true - regjsparser@0.13.1: + regjsparser@0.13.2: dependencies: jsesc: 3.1.0 - rename-keys@1.2.0: {} - - request-promise@3.0.0: - dependencies: - bluebird: 3.7.2 - lodash: 4.18.1 - request: 2.88.2 - - request@2.88.2: - dependencies: - aws-sign2: 0.7.0 - aws4: 1.13.2 - caseless: 0.12.0 - combined-stream: 1.0.8 - extend: 3.0.2 - forever-agent: 0.6.1 - form-data: 2.3.3 - har-validator: 5.1.5 - http-signature: 1.2.0 - is-typedarray: 1.0.0 - isstream: 0.1.2 - json-stringify-safe: 5.0.1 - mime-types: 2.1.35 - oauth-sign: 0.9.0 - performance-now: 2.1.0 - qs: 6.5.5 - safe-buffer: 5.2.1 - tough-cookie: 2.5.0 - tunnel-agent: 0.6.0 - uuid: 3.4.0 - require-from-string@2.0.2: {} requires-port@1.0.0: {} + reserved-identifiers@1.2.0: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -9326,44 +8424,36 @@ snapshots: object-keys: 1.1.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - - restore-cursor@1.0.1: - dependencies: - exit-hook: 1.1.1 - onetime: 1.1.0 + optional: true reusify@1.1.0: {} - rimraf@2.7.1: - dependencies: - glob: 7.2.3 - robust-predicates@3.0.3: {} - rolldown-license-plugin@3.0.9(rolldown@1.0.3): + rolldown-license-plugin@3.0.9(rolldown@1.1.5): dependencies: - rolldown: 1.0.3 + rolldown: 1.1.5 - rolldown@1.0.3: + rolldown@1.1.5: dependencies: - '@oxc-project/types': 0.133.0 + '@oxc-project/types': 0.139.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 roughjs@4.6.6: dependencies: @@ -9372,10 +8462,6 @@ snapshots: points-on-curve: 0.2.0 points-on-path: 0.2.1 - run-async@0.1.0: - dependencies: - once: 1.4.0 - run-con@1.3.2: dependencies: deep-extend: 0.6.0 @@ -9389,8 +8475,6 @@ snapshots: rw@1.3.3: {} - rx-lite@3.1.2: {} - safe-array-concat@1.1.4: dependencies: call-bind: 1.0.9 @@ -9398,19 +8482,20 @@ snapshots: get-intrinsic: 1.3.0 has-symbols: 1.1.0 isarray: 2.0.5 - - safe-buffer@5.2.1: {} + optional: true safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 isarray: 2.0.5 + optional: true safe-regex-test@1.1.0: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 is-regex: 1.2.1 + optional: true safer-buffer@2.1.2: {} @@ -9426,9 +8511,12 @@ snapshots: refa: 0.12.1 regexp-ast-analysis: 0.7.1 - semver@6.3.1: {} + semver@6.3.1: + optional: true - semver@7.8.1: {} + semver@7.8.4: {} + + semver@7.8.5: {} seroval-plugins@1.5.4(seroval@1.5.4): dependencies: @@ -9444,6 +8532,7 @@ snapshots: get-intrinsic: 1.3.0 gopd: 1.2.0 has-property-descriptors: 1.0.2 + optional: true set-function-name@2.0.2: dependencies: @@ -9451,12 +8540,14 @@ snapshots: es-errors: 1.3.0 functions-have-names: 1.2.3 has-property-descriptors: 1.0.2 + optional: true set-proto@1.0.0: dependencies: dunder-proto: 1.0.1 es-errors: 1.3.0 es-object-atoms: 1.1.2 + optional: true shebang-command@2.0.0: dependencies: @@ -9468,6 +8559,7 @@ snapshots: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 + optional: true side-channel-map@1.0.1: dependencies: @@ -9475,6 +8567,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 object-inspect: 1.13.4 + optional: true side-channel-weakmap@1.0.2: dependencies: @@ -9483,6 +8576,7 @@ snapshots: get-intrinsic: 1.3.0 object-inspect: 1.13.4 side-channel-map: 1.0.1 + optional: true side-channel@1.1.0: dependencies: @@ -9491,6 +8585,7 @@ snapshots: side-channel-list: 1.0.1 side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + optional: true siginfo@2.0.0: {} @@ -9527,22 +8622,10 @@ snapshots: source-map@0.6.1: optional: true - spectral-cli-bundle@1.0.8: + spectral-cli-bundle@1.0.11: optionalDependencies: fsevents: 2.3.3 - sshpk@1.18.0: - dependencies: - asn1: 0.2.6 - assert-plus: 1.0.0 - bcrypt-pbkdf: 1.0.2 - dashdash: 1.14.1 - ecc-jsbn: 0.1.2 - getpass: 0.1.7 - jsbn: 0.1.1 - safer-buffer: 2.1.2 - tweetnacl: 0.14.5 - stable-hash-x@0.2.0: {} stack-utils@2.0.6: @@ -9557,12 +8640,7 @@ snapshots: dependencies: es-errors: 1.3.0 internal-slot: 1.1.0 - - string-width@1.0.2: - dependencies: - code-point-at: 1.1.0 - is-fullwidth-code-point: 1.0.0 - strip-ansi: 3.0.1 + optional: true string-width@4.2.3: dependencies: @@ -9570,22 +8648,11 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - string-width@8.1.0: - dependencies: - get-east-asian-width: 1.6.0 - strip-ansi: 7.2.0 - string-width@8.2.1: dependencies: get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 - string.prototype.includes@2.0.1: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - string.prototype.trim@1.2.10: dependencies: call-bind: 1.0.9 @@ -9595,6 +8662,7 @@ snapshots: es-abstract: 1.24.2 es-object-atoms: 1.1.2 has-property-descriptors: 1.0.2 + optional: true string.prototype.trimend@1.0.9: dependencies: @@ -9602,16 +8670,14 @@ snapshots: call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.2 + optional: true string.prototype.trimstart@1.0.8: dependencies: call-bind: 1.0.9 define-properties: 1.2.1 es-object-atoms: 1.1.2 - - strip-ansi@3.0.1: - dependencies: - ansi-regex: 2.1.1 + optional: true strip-ansi@6.0.1: dependencies: @@ -9621,7 +8687,8 @@ snapshots: dependencies: ansi-regex: 6.2.2 - strip-bom@3.0.0: {} + strip-bom@3.0.0: + optional: true strip-indent@4.1.1: {} @@ -9631,35 +8698,35 @@ snapshots: style-search@0.1.0: {} - stylelint-config-recommended@18.0.0(stylelint@17.13.0(typescript@6.0.3)): + stylelint-config-recommended@18.0.0(stylelint@17.14.0(typescript@6.0.3)): dependencies: - stylelint: 17.13.0(typescript@6.0.3) + stylelint: 17.14.0(typescript@6.0.3) - stylelint-declaration-block-no-ignored-properties@3.0.0(stylelint@17.13.0(typescript@6.0.3)): + stylelint-declaration-block-no-ignored-properties@3.0.0(stylelint@17.14.0(typescript@6.0.3)): dependencies: - stylelint: 17.13.0(typescript@6.0.3) + stylelint: 17.14.0(typescript@6.0.3) - stylelint-declaration-strict-value@1.11.1(stylelint@17.13.0(typescript@6.0.3)): + stylelint-declaration-strict-value@1.11.1(stylelint@17.14.0(typescript@6.0.3)): dependencies: - stylelint: 17.13.0(typescript@6.0.3) + stylelint: 17.14.0(typescript@6.0.3) - stylelint-value-no-unknown-custom-properties@6.1.1(stylelint@17.13.0(typescript@6.0.3)): + stylelint-value-no-unknown-custom-properties@6.1.1(stylelint@17.14.0(typescript@6.0.3)): dependencies: postcss-value-parser: 4.2.0 resolve: 1.22.12 - stylelint: 17.13.0(typescript@6.0.3) + stylelint: 17.14.0(typescript@6.0.3) - stylelint@17.13.0(typescript@6.0.3): + stylelint@17.14.0(typescript@6.0.3): dependencies: '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-syntax-patches-for-csstree': 1.1.4(css-tree@3.2.1) + '@csstools/css-syntax-patches-for-csstree': 1.1.6(css-tree@3.2.1) '@csstools/css-tokenizer': 4.0.0 '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1) - '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1) + '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.4) + '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.4) colord: 2.9.3 - cosmiconfig: 9.0.1(typescript@6.0.3) + cosmiconfig: 9.0.2(typescript@6.0.3) css-functions-list: 3.3.3 css-tree: 3.2.1 debug: 4.4.3 @@ -9677,9 +8744,9 @@ snapshots: micromatch: 4.0.8 normalize-path: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.15 - postcss-safe-parser: 7.0.1(postcss@8.5.15) - postcss-selector-parser: 7.1.1 + postcss: 8.5.16 + postcss-safe-parser: 7.0.1(postcss@8.5.16) + postcss-selector-parser: 7.1.4 postcss-value-parser: 4.2.0 string-width: 8.2.1 supports-hyperlinks: 4.4.0 @@ -9702,9 +8769,13 @@ snapshots: tinyglobby: 0.2.17 ts-interface-checker: 0.1.13 - supports-color@10.2.2: {} + super-regex@1.1.0: + dependencies: + function-timeout: 1.0.2 + make-asynchronous: 1.1.0 + time-span: 5.1.0 - supports-color@2.0.0: {} + supports-color@10.2.2: {} supports-color@7.2.0: dependencies: @@ -9717,8 +8788,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svg-element-attributes@1.3.1: {} - svg-tags@1.0.0: {} svgo@4.0.1: @@ -9731,12 +8800,7 @@ snapshots: picocolors: 1.1.1 sax: 1.6.0 - svgson@5.3.1: - dependencies: - deep-rename-keys: 0.2.1 - xml-reader: 2.4.3 - - swagger-ui-dist@5.32.6: + swagger-ui-dist@5.32.8: dependencies: '@scarf/scarf': 1.4.0 @@ -9749,10 +8813,6 @@ snapshots: transitivePeerDependencies: - encoding - synckit@0.11.13: - dependencies: - '@pkgr/core': 0.3.6 - table@6.9.0: dependencies: ajv: 8.20.0 @@ -9761,7 +8821,7 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - tailwindcss@3.4.19: + tailwindcss@3.4.19(yaml@2.9.0): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -9777,11 +8837,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.15 - postcss-import: 15.1.0(postcss@8.5.15) - postcss-js: 4.1.0(postcss@8.5.15) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.15) - postcss-nested: 6.2.0(postcss@8.5.15) + postcss: 8.5.16 + postcss-import: 15.1.0(postcss@8.5.16) + postcss-js: 4.1.0(postcss@8.5.16) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.16)(yaml@2.9.0) + postcss-nested: 6.2.0(postcss@8.5.16) postcss-selector-parser: 6.1.2 resolve: 1.22.12 sucrase: 3.35.1 @@ -9801,7 +8861,9 @@ snapshots: throttle-debounce@5.0.2: {} - through@2.3.8: {} + time-span@5.1.0: + dependencies: + convert-hrtime: 5.0.0 tinybench@2.9.0: {} @@ -9809,8 +8871,8 @@ snapshots: tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tinyrainbow@3.1.0: {} @@ -9824,11 +8886,6 @@ snapshots: toastify-js@1.12.0: {} - tough-cookie@2.5.0: - dependencies: - psl: 1.15.0 - punycode: 2.3.1 - tough-cookie@4.1.4: dependencies: psl: 1.15.0 @@ -9844,10 +8901,6 @@ snapshots: tributejs@5.1.3: {} - ts-api-utils@2.5.0(typescript@5.9.3): - dependencies: - typescript: 5.9.3 - ts-api-utils@2.5.0(typescript@6.0.3): dependencies: typescript: 6.0.3 @@ -9862,26 +8915,24 @@ snapshots: json5: 1.0.2 minimist: 1.2.8 strip-bom: 3.0.0 + optional: true tslib@2.8.1: {} - tunnel-agent@0.6.0: - dependencies: - safe-buffer: 5.2.1 - - tweetnacl@0.14.5: {} - type-check@0.4.0: dependencies: prelude-ls: 1.2.1 type-detect@4.0.8: {} + type-fest@4.41.0: {} + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 is-typed-array: 1.1.15 + optional: true typed-array-byte-length@1.0.3: dependencies: @@ -9890,6 +8941,7 @@ snapshots: gopd: 1.2.0 has-proto: 1.2.0 is-typed-array: 1.1.15 + optional: true typed-array-byte-offset@1.0.4: dependencies: @@ -9900,6 +8952,7 @@ snapshots: has-proto: 1.2.0 is-typed-array: 1.1.15 reflect.getprototypeof: 1.0.10 + optional: true typed-array-length@1.0.8: dependencies: @@ -9909,31 +8962,19 @@ snapshots: is-typed-array: 1.1.15 possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + optional: true - typescript-eslint@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3): + typescript-eslint@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/parser': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) - eslint: 10.4.1(jiti@2.7.0) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - typescript-eslint@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/parser': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.61.0(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.4.1(jiti@2.7.0) + '@typescript-eslint/eslint-plugin': 8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.63.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - typescript@5.9.3: {} - typescript@6.0.3: {} typo-js@1.3.2: {} @@ -9948,8 +8989,9 @@ snapshots: has-bigints: 1.1.0 has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + optional: true - undici-types@7.24.6: {} + undici-types@8.3.0: {} unicorn-magic@0.4.0: {} @@ -9982,15 +9024,19 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 - untildify@3.0.3: {} - update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 escalade: 3.2.0 picocolors: 1.1.1 - updates@17.18.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.6): + dependencies: + browserslist: 4.28.6 + escalade: 3.2.0 + picocolors: 1.1.1 + + updates@17.18.2: {} uri-js@4.4.1: dependencies: @@ -10001,108 +9047,91 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 - user-home@2.0.0: - dependencies: - os-homedir: 1.0.2 - util-deprecate@1.0.2: {} uuid@14.0.0: {} - uuid@3.4.0: {} - vanilla-colorful@0.7.2: {} - verror@1.10.0: + vite-string-plugin@2.0.5(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): dependencies: - assert-plus: 1.0.0 - core-util-is: 1.0.2 - extsprintf: 1.3.0 + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) - vite-string-plugin@2.0.4(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)): - dependencies: - vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0) - - vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0): + vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 - rolldown: 1.0.3 + picomatch: 4.0.5 + postcss: 8.5.16 + rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.9.3 + '@types/node': 26.1.1 esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 + yaml: 2.9.0 - vitest@4.1.8(@types/node@25.9.3)(happy-dom@20.10.2)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)): + vitest@4.1.10(@types/node@26.1.1)(happy-dom@20.10.6)(jsdom@20.0.3)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)): dependencies: - '@vitest/expect': 4.1.8 - '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)) - '@vitest/pretty-format': 4.1.8 - '@vitest/runner': 4.1.8 - '@vitest/snapshot': 4.1.8 - '@vitest/spy': 4.1.8 - '@vitest/utils': 4.1.8 + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.4 + picomatch: 4.0.5 std-env: 4.1.0 tinybench: 2.9.0 tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0) + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.9.3 - happy-dom: 20.10.2 + '@types/node': 26.1.1 + happy-dom: 20.10.6 jsdom: 20.0.3 transitivePeerDependencies: - msw vscode-uri@3.1.0: {} - vue-bar-graph@2.2.0(typescript@6.0.3): - dependencies: - vue: 3.5.37(typescript@6.0.3) - transitivePeerDependencies: - - typescript - - vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.37(typescript@6.0.3)): + vue-chartjs@5.3.4(chart.js@4.5.1)(vue@3.5.39(typescript@6.0.3)): dependencies: chart.js: 4.5.1 - vue: 3.5.37(typescript@6.0.3) + vue: 3.5.39(typescript@6.0.3) - vue-eslint-parser@10.4.0(eslint@10.4.1(jiti@2.7.0)): + vue-eslint-parser@10.4.0(eslint@10.6.0(jiti@2.7.0)): dependencies: debug: 4.4.3 - eslint: 10.4.1(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 espree: 11.2.0 esquery: 1.7.0 - semver: 7.8.1 + semver: 7.8.4 transitivePeerDependencies: - supports-color - vue-tsc@3.3.4(typescript@6.0.3): + vue-tsc@3.3.7(typescript@6.0.3): dependencies: '@volar/typescript': 2.4.28 - '@vue/language-core': 3.3.4 + '@vue/language-core': 3.3.7 typescript: 6.0.3 - vue@3.5.37(typescript@6.0.3): + vue@3.5.39(typescript@6.0.3): dependencies: - '@vue/compiler-dom': 3.5.37 - '@vue/compiler-sfc': 3.5.37 - '@vue/runtime-dom': 3.5.37 - '@vue/server-renderer': 3.5.37(vue@3.5.37(typescript@6.0.3)) - '@vue/shared': 3.5.37 + '@vue/compiler-dom': 3.5.39 + '@vue/compiler-sfc': 3.5.39 + '@vue/runtime-dom': 3.5.39 + '@vue/server-renderer': 3.5.39(vue@3.5.39(typescript@6.0.3)) + '@vue/shared': 3.5.39 optionalDependencies: typescript: 6.0.3 @@ -10112,6 +9141,8 @@ snapshots: dependencies: xml-name-validator: 4.0.0 + web-worker@1.5.0: {} + webidl-conversions@3.0.1: {} webidl-conversions@7.0.0: {} @@ -10139,6 +9170,7 @@ snapshots: is-number-object: 1.1.1 is-string: 1.1.1 is-symbol: 1.1.1 + optional: true which-builtin-type@1.2.1: dependencies: @@ -10155,6 +9187,7 @@ snapshots: which-boxed-primitive: 1.1.1 which-collection: 1.0.2 which-typed-array: 1.1.21 + optional: true which-collection@1.0.2: dependencies: @@ -10162,6 +9195,7 @@ snapshots: is-set: 2.0.3 is-weakmap: 2.0.2 is-weakset: 2.0.4 + optional: true which-typed-array@1.1.21: dependencies: @@ -10172,6 +9206,7 @@ snapshots: get-proto: 1.0.1 gopd: 1.2.0 has-tostringtag: 1.0.2 + optional: true which@1.3.1: dependencies: @@ -10188,25 +9223,16 @@ snapshots: word-wrap@1.2.5: {} - wrappy@1.0.2: {} - write-file-atomic@7.0.1: dependencies: signal-exit: 4.1.0 ws@8.21.0: {} - xml-lexer@0.2.2: - dependencies: - eventemitter3: 2.0.3 - xml-name-validator@4.0.0: {} - xml-reader@2.4.3: - dependencies: - eventemitter3: 2.0.3 - xml-lexer: 0.2.2 - xmlchars@2.2.0: {} + yaml@2.9.0: {} + yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c16cfb76cb..8f1c49389b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,10 +3,6 @@ dedupePeerDependents: false updateNotifier: false minimumReleaseAge: 0 -peerDependencyRules: - allowedVersions: - eslint-plugin-github>eslint: '>=9' - allowBuilds: '@scarf/scarf': false core-js: false diff --git a/public/assets/img/svg/octicon-graph-stacked-area.svg b/public/assets/img/svg/octicon-graph-stacked-area.svg new file mode 100644 index 0000000000..a852acbdf1 --- /dev/null +++ b/public/assets/img/svg/octicon-graph-stacked-area.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/img/svg/octicon-project-check.svg b/public/assets/img/svg/octicon-project-check.svg new file mode 100644 index 0000000000..d8510203e1 --- /dev/null +++ b/public/assets/img/svg/octicon-project-check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/img/svg/octicon-repo-forked-locked.svg b/public/assets/img/svg/octicon-repo-forked-locked.svg index 4a2b35301f..3de3edf7c8 100644 --- a/public/assets/img/svg/octicon-repo-forked-locked.svg +++ b/public/assets/img/svg/octicon-repo-forked-locked.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/assets/img/svg/octicon-view-files.svg b/public/assets/img/svg/octicon-view-files.svg new file mode 100644 index 0000000000..419e0d780a --- /dev/null +++ b/public/assets/img/svg/octicon-view-files.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 631d0a5052..2a0880a2db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,9 +5,9 @@ requires-python = ">=3.10" [dependency-groups] dev = [ - "djlint==1.39.0", + "djlint==1.40.4", "yamllint==1.38.0", - "zizmor==1.25.2", + "zizmor==1.26.1", ] [tool.djlint] diff --git a/renovate.json5 b/renovate.json5 index bbfbb8d78a..f7acc11135 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -6,11 +6,13 @@ "labels": ["dependencies"], "branchPrefix": "renovate/", "schedule": ["* * * * 1"], // dependency update PRs weekly, vulnerabilityAlerts bypasses this + "separateMajorMinor": false, // include major updates in group PRs "minimumReleaseAge": "5 days", "semanticCommits": "enabled", "osvVulnerabilityAlerts": true, "vulnerabilityAlerts": { "enabled": true, + "semanticCommitType": "fix", // overrides packageRules }, "customManagers": [ { @@ -23,6 +25,10 @@ }, ], "packageRules": [ + { + "matchPackageNames": ["*"], + "semanticCommitType": "chore", // dep updates are rarely fixes for gitea + }, { "groupName": "action dependencies", "matchManagers": ["github-actions"], @@ -39,6 +45,14 @@ "matchPackageNames": ["tailwindcss"], "allowedVersions": "^3", // need to migrate }, + { + "matchPackageNames": ["@citation-js/core", "@citation-js/plugin-bibtex", "@citation-js/plugin-csl"], + "allowedVersions": "<0.8", // https://github.com/citation-js/citation-js/pull/277 + }, + { + "matchPackageNames": ["typescript"], + "allowedVersions": "^6", // typescript-eslint is not yet compatible with typescript 7 + }, { "matchPackageNames": ["github.com/Azure/azure-sdk-for-go/sdk/azcore"], "allowedVersions": "<1.21.0", // v1.21.0+ uses API version unsupported by Azurite in CI @@ -73,7 +87,11 @@ "postUpdateOptions": ["gomodUpdateImportPaths"], "postUpgradeTasks": { "commands": ["make tidy"], - "fileFilters": ["go.mod", "go.sum", "assets/go-licenses.json"], + "fileFilters": [ + "go.mod", + "go.sum", + "assets/go-licenses.json", + ], "executionMode": "branch", }, }, @@ -95,8 +113,15 @@ "matchManagers": ["npm"], "postUpdateOptions": ["pnpmDedupe"], "postUpgradeTasks": { - "commands": ["make svg"], - "fileFilters": ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", "public/assets/img/svg/**"], + "commands": ["make svg", "make generate-codemirror-languages"], + "fileFilters": [ + "package.json", + "pnpm-lock.yaml", + "pnpm-workspace.yaml", + "public/assets/img/svg/**", + "options/fileicon/**", + "assets/codemirror-languages.json", + ], "executionMode": "branch", }, }, diff --git a/routers/api/actions/runner/interceptor.go b/routers/api/actions/runner/interceptor.go index a3508aa9b1..ec1ba58e91 100644 --- a/routers/api/actions/runner/interceptor.go +++ b/routers/api/actions/runner/interceptor.go @@ -8,6 +8,7 @@ import ( "crypto/subtle" "errors" "strings" + "time" actions_model "gitea.dev/models/actions" auth_model "gitea.dev/models/auth" @@ -45,14 +46,26 @@ var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unar return nil, status.Error(codes.Unauthenticated, "unregistered runner") } - cols := []string{"last_online"} - runner.LastOnline = timeutil.TimeStampNow() - if methodName == "UpdateTask" || methodName == "UpdateLog" { - runner.LastActive = timeutil.TimeStampNow() + now := time.Now() + cols := make([]string, 0, 2) + // Debounce last_active too: while a runner streams logs, UpdateLog fires + // many times per second and writing on each is a major source of DB load. + // Persist only when stale enough to affect the active/idle status. + if (methodName == "UpdateTask" || methodName == "UpdateLog") && + actions_model.ShouldPersistLastActive(runner.LastActive, now) { + runner.LastActive = timeutil.TimeStamp(now.Unix()) cols = append(cols, "last_active") } - if err := actions_model.UpdateRunner(ctx, runner, cols...); err != nil { - log.Error("can't update runner status: %v", err) + // Debounce last_online: writing on every poll is a major source of DB load + // with many runners. Persist only when stale enough to affect offline status. + if actions_model.ShouldPersistLastOnline(runner.LastOnline, now) { + runner.LastOnline = timeutil.TimeStamp(now.Unix()) + cols = append(cols, "last_online") + } + if len(cols) > 0 { + if err := actions_model.UpdateRunner(ctx, runner, cols...); err != nil { + log.Error("can't update runner status: %v", err) + } } ctx = context.WithValue(ctx, runnerCtxKey{}, runner) diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go index 012f875ec7..44e0151c86 100644 --- a/routers/api/actions/runner/runner.go +++ b/routers/api/actions/runner/runner.go @@ -194,9 +194,15 @@ func (s *Service) FetchTask( // if the task version in request is not equal to the version in db, // it means there may still be some tasks that haven't been assigned. // try to pick a task for the runner that send the request. - if t, ok, err := actions_service.PickTask(ctx, freshRunner); err != nil { + if t, ok, throttled, err := actions_service.TryPickTask(ctx, freshRunner); err != nil { log.Error("pick task failed: %v", err) return nil, status.Errorf(codes.Internal, "pick task: %v", err) + } else if throttled { + // Concurrency limit reached: don't advance the runner's tasks version, + // so it retries on its next poll instead of sleeping until the next bump. + latestVersion = tasksVersion + // A steady stream here means MAX_CONCURRENT_TASK_PICKS is too low for the fleet. + log.Debug("task pick throttled for runner %q (id %d); it will retry on its next poll", freshRunner.Name, freshRunner.ID) } else if ok { task = t } diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go index d3560dc4b9..9a593f7caa 100644 --- a/routers/api/packages/alpine/alpine.go +++ b/routers/api/packages/alpine/alpine.go @@ -67,15 +67,34 @@ func GetRepositoryFile(ctx *context.Context) { return } + branch := ctx.PathParam("branch") + repository := ctx.PathParam("repository") + architecture := ctx.PathParam("architecture") + s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion( ctx, pv, &packages_service.PackageFileInfo{ Filename: alpine_service.IndexArchiveFilename, - CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.PathParam("branch"), ctx.PathParam("repository"), ctx.PathParam("architecture")), + CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture), }, ctx.Req.Method, ) + // A repository that only contains "noarch" packages has no per-architecture + // index. Since noarch packages are installable on every architecture, fall + // back to the noarch index so clients requesting their own architecture + // (e.g. x86_64) can still discover them. + if errors.Is(err, util.ErrNotExist) && architecture != alpine_module.NoArch { + s, u, pf, err = packages_service.OpenFileForDownloadByPackageVersion( + ctx, + pv, + &packages_service.PackageFileInfo{ + Filename: alpine_service.IndexArchiveFilename, + CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, alpine_module.NoArch), + }, + ctx.Req.Method, + ) + } if err != nil { if errors.Is(err, util.ErrNotExist) { apiError(ctx, http.StatusNotFound, err) diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 3de53929e9..3da0ee4adc 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -73,7 +73,9 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) { } if publicOnly { - if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() { + // a public-only token must not reach limited-visibility owners either, + // matching how orgs/users are enforced elsewhere in this file + if ctx.Package != nil && !ctx.Package.Owner.Visibility.IsPublic() { ctx.HTTPError(http.StatusForbidden, "reqToken", "token scope is limited to public packages") return } diff --git a/routers/api/packages/debian/debian.go b/routers/api/packages/debian/debian.go index f9bf2960f8..ee9d4d554a 100644 --- a/routers/api/packages/debian/debian.go +++ b/routers/api/packages/debian/debian.go @@ -120,9 +120,9 @@ func GetRepositoryFileByHash(ctx *context.Context) { } func UploadPackageFile(ctx *context.Context) { - distribution := strings.TrimSpace(ctx.PathParam("distribution")) - component := strings.TrimSpace(ctx.PathParam("component")) - if distribution == "" || component == "" { + distribution := ctx.PathParam("distribution") + component := ctx.PathParam("component") + if !debian_module.IsValidDistributionOrComponent(distribution) || !debian_module.IsValidDistributionOrComponent(component) { apiError(ctx, http.StatusBadRequest, "invalid distribution or component") return } diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go index 9c92cddaa8..8aef046b62 100644 --- a/routers/api/packages/npm/npm.go +++ b/routers/api/packages/npm/npm.go @@ -333,11 +333,18 @@ func ListPackageTags(ctx *context.Context) { func AddPackageTag(ctx *context.Context) { packageName := packageNameFromParams(ctx) - body, err := io.ReadAll(ctx.Req.Body) + // the dist-tag body is only a quoted version string; bound it to avoid an unbounded + // read that could exhaust memory + const maxDistTagBodySize = 4 * 1024 + body, err := io.ReadAll(io.LimitReader(ctx.Req.Body, maxDistTagBodySize+1)) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } + if len(body) > maxDistTagBodySize { + apiError(ctx, http.StatusRequestEntityTooLarge, errors.New("request body too large")) + return + } version := strings.Trim(string(body), "\"") // is as "version" in the body pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName, version) diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go index 8f8b9fd0ec..ac625bd2a2 100644 --- a/routers/api/v1/admin/adopt.go +++ b/routers/api/v1/admin/adopt.go @@ -99,7 +99,7 @@ func AdoptRepository(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } - exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName))) + exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.CodeRepoByName(ctxUser.Name, repoName)) if err != nil { ctx.APIErrorInternal(err) return @@ -161,7 +161,7 @@ func DeleteUnadoptedRepository(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } - exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName))) + exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.CodeRepoByName(ctxUser.Name, repoName)) if err != nil { ctx.APIErrorInternal(err) return diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go index 4713c7a1c7..2e1262589d 100644 --- a/routers/api/v1/admin/org.go +++ b/routers/api/v1/admin/org.go @@ -48,7 +48,7 @@ func CreateOrg(ctx *context.APIContext) { visibility := api.VisibleTypePublic if form.Visibility != "" { - visibility = api.VisibilityModes[string(form.Visibility)] + visibility = api.VisibilityModes[form.Visibility] } org := &organization.Organization{ diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 9441fe0c18..ec0d5eb3ce 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -121,7 +121,7 @@ func CreateUser(ctx *context.APIContext) { } if form.Visibility != "" { - visibility := api.VisibilityModes[string(form.Visibility)] + visibility := api.VisibilityModes[form.Visibility] overwriteDefault.Visibility = &visibility } @@ -237,7 +237,7 @@ func EditUser(ctx *context.APIContext) { Description: optional.FromPtr(form.Description), IsActive: optional.FromPtr(form.Active), IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin), - Visibility: optional.FromMapLookup(api.VisibilityModes, string(form.Visibility)), + Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility), AllowGitHook: optional.FromPtr(form.AllowGitHook), AllowImportLocal: optional.FromPtr(form.AllowImportLocal), MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation), @@ -469,7 +469,7 @@ func SearchUsers(ctx *context.APIContext) { var visible []api.VisibleType visibilityParam := ctx.FormString("visibility") - if visibility, ok := api.VisibilityModes[visibilityParam]; ok { + if visibility, ok := api.VisibilityModes[api.VisibilityString(visibilityParam)]; ok { visible = []api.VisibleType{visibility} } else if visibilityParam != "" { ctx.APIError(http.StatusUnprocessableEntity, "invalid visibility") diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index a76563496c..fa8d29c28b 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -99,6 +99,7 @@ import ( _ "gitea.dev/routers/api/v1/swagger" // for swagger generation "gitea.com/go-chi/binding" + chi_middleware "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" ) @@ -290,7 +291,9 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) { return } case auth_model.AccessTokenScopeCategoryPackage: - if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() { + // a public-only token must not reach limited-visibility owners either, + // matching the org/user public-only enforcement above + if ctx.Package != nil && !ctx.Package.Owner.Visibility.IsPublic() { ctx.APIError(http.StatusForbidden, "token scope is limited to public packages") return } @@ -950,6 +953,9 @@ func checkDeprecatedAuthMethods(ctx *context.APIContext) { func Routes() *web.Router { m := web.NewRouter() + // redirect HEAD requests to GET if no HEAD handler is defined (RFC 9110 §9.3.2) + m.BeforeRouting(chi_middleware.GetHead) + if setting.CORSConfig.Enabled { m.BeforeRouting(cors.Handler(cors.Options{ AllowedOrigins: setting.CORSConfig.AllowDomain, @@ -1719,7 +1725,7 @@ func Routes() *web.Router { m.Get("", reqToken(), org.ListMembers) m.Combo("/{username}").Get(reqToken(), org.IsMember). Delete(reqToken(), reqOrgOwnership(), org.DeleteMember) - }) + }, reqOrgVisible()) addActionsRoutes( m, reqOrgMembership(), @@ -1731,7 +1737,7 @@ func Routes() *web.Router { m.Combo("/{username}").Get(org.IsPublicMember). Put(reqToken(), reqOrgMembership(), org.PublicizeMember). Delete(reqToken(), reqOrgMembership(), org.ConcealMember) - }) + }, reqOrgVisible()) m.Group("/teams", func() { m.Get("", org.ListTeams) m.Post("", reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam) diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index 8f4d19719f..196b1c0fc9 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -145,6 +145,13 @@ func GetUserOrgsPermissions(ctx *context.APIContext) { op := api.OrganizationPermissions{} + // A public-only token must not disclose membership/permission details of a + // non-public org, even for the token owner's own private orgs. + if ctx.PublicOnly && !o.Visibility.IsPublic() { + ctx.APIErrorNotFound() + return + } + if !organization.HasOrgOrUserVisible(ctx, o, ctx.Doer) { ctx.APIErrorNotFound() return @@ -262,7 +269,7 @@ func Create(ctx *context.APIContext) { visibility := api.VisibleTypePublic if form.Visibility != "" { - visibility = api.VisibilityModes[string(form.Visibility)] + visibility = api.VisibilityModes[form.Visibility] } org := &organization.Organization{ @@ -406,7 +413,7 @@ func Edit(ctx *context.APIContext) { Description: optional.FromPtr(form.Description), Website: optional.FromPtr(form.Website), Location: optional.FromPtr(form.Location), - Visibility: optional.FromMapLookup(api.VisibilityModes, string(optional.FromPtr(form.Visibility).Value())), + Visibility: optional.FromMapLookup(api.VisibilityModes, optional.FromPtr(form.Visibility).Value()), RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess), } if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil { diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index bc6bb54e90..f57b997a15 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -223,7 +223,7 @@ func CreateTeam(ctx *context.APIContext) { IncludesAllRepositories: form.IncludesAllRepositories, CanCreateOrgRepo: form.CanCreateOrgRepo, AccessMode: teamPermission, - Visibility: organization.NormalizeTeamVisibility(string(form.Visibility)), + Visibility: organization.NormalizeTeamVisibility(form.Visibility), } if team.AccessMode < perm.AccessModeAdmin { @@ -302,7 +302,7 @@ func EditTeam(ctx *context.APIContext) { } if form.Visibility != nil && !team.IsOwnerTeam() { - team.Visibility = organization.NormalizeTeamVisibility(string(*form.Visibility)) + team.Visibility = organization.NormalizeTeamVisibility(*form.Visibility) } isAuthChanged := false @@ -567,10 +567,20 @@ func GetTeamRepos(ctx *context.APIContext) { team := ctx.Org.Team listOptions := utils.GetListOptions(ctx) - teamRepos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{ + // A public-only token must not expose (or count) private repos, even when the + // doer owning the token otherwise has access to them, so filter them out at the + // query level to keep the returned page and the total-count header consistent. + searchOpts := &repo_model.SearchTeamRepoOptions{ ListOptions: listOptions, TeamID: team.ID, - }) + PublicOnly: ctx.PublicOnly, + } + teamRepos, err := repo_model.GetTeamRepositories(ctx, searchOpts) + if err != nil { + ctx.APIErrorInternal(err) + return + } + count, err := repo_model.CountTeamRepositories(ctx, searchOpts) if err != nil { ctx.APIErrorInternal(err) return @@ -584,14 +594,16 @@ func GetTeamRepos(ctx *context.APIContext) { } // A team's repo list is reachable by non-team-members through the team's // visibility tier, so never expose repos (incl. their names) the doer - // cannot access. + // cannot access. This per-repo visibility trim can't be expressed in the + // SQL count above without regressing per-unit public access, so for such + // non-members the total-count header may be a small upper bound. if !permission.HasAnyUnitAccessOrPublicAccess() { continue } repos = append(repos, convert.ToRepo(ctx, repo, permission)) } - ctx.SetLinkHeader(int64(team.NumRepos), listOptions.PageSize) - ctx.SetTotalCountHeader(int64(team.NumRepos)) + ctx.SetLinkHeader(count, listOptions.PageSize) + ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, repos) } @@ -630,6 +642,12 @@ func GetTeamRepo(ctx *context.APIContext) { return } + // A public-only token must not confirm the existence of a private repo. + if !ctx.TokenCanAccessRepo(repo) { + ctx.APIErrorNotFound() + return + } + if !organization.HasTeamRepo(ctx, ctx.Org.Team.OrgID, ctx.Org.Team.ID, repo.ID) { ctx.APIErrorNotFound() return @@ -664,6 +682,22 @@ func getRepositoryByParams(ctx *context.APIContext) *repo_model.Repository { return repo } +func canChangeTeamRepository(ctx *context.APIContext) bool { + if ctx.Org.Organization.RepoAdminChangeTeamAccess { + return true + } + isOwner, err := ctx.Org.Organization.IsOwnedBy(ctx, ctx.Doer.ID) + if err != nil { + ctx.APIErrorInternal(err) + return false + } + if !isOwner { + ctx.APIError(http.StatusForbidden, "user is nor repo admin nor owner") + return false + } + return true +} + // AddTeamRepository api for adding a repository to a team func AddTeamRepository(ctx *context.APIContext) { // swagger:operation PUT /teams/{id}/repos/{org}/{repo} organization orgAddTeamRepository @@ -700,6 +734,9 @@ func AddTeamRepository(ctx *context.APIContext) { if ctx.Written() { return } + if !canChangeTeamRepository(ctx) { + return + } if access, err := access_model.AccessLevel(ctx, ctx.Doer, repo); err != nil { ctx.APIErrorInternal(err) return @@ -752,6 +789,9 @@ func RemoveTeamRepository(ctx *context.APIContext) { if ctx.Written() { return } + if !canChangeTeamRepository(ctx) { + return + } if access, err := access_model.AccessLevel(ctx, ctx.Doer, repo); err != nil { ctx.APIErrorInternal(err) return @@ -889,6 +929,8 @@ func ListTeamActivityFeeds(ctx *context.APIContext) { Date: ctx.FormString("date"), ListOptions: listOptions, } + // A public-only token must not receive private activity entries. + opts.ApplyPublicOnly(ctx.PublicOnly) feeds, count, err := feed_service.GetFeeds(ctx, opts) if err != nil { diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 3b920ac551..b1c500d4d5 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -36,11 +36,11 @@ import ( "gitea.com/gitea/runner/act/model" ) -// ListActionsSecrets list an repo's actions secrets +// ListActionsSecrets list a repo's actions secrets func (Action) ListActionsSecrets(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/secrets repository repoListActionsSecrets // --- - // summary: List an repo's actions secrets + // summary: List a repo's actions secrets // produces: // - application/json // parameters: @@ -1024,6 +1024,11 @@ func ActionsListWorkflowRuns(ctx *context.APIContext) { // description: if true, the `pull_requests` field on each returned run is emptied // type: boolean // required: false + // - name: scoped_workflow_source_repo_id + // description: For a scoped workflow, the ID of the source repository providing it; omit or 0 for a repo-level workflow. + // in: query + // type: integer + // format: int64 // - name: page // in: query // description: page number of results to return (1-based) @@ -1043,20 +1048,25 @@ func ActionsListWorkflowRuns(ctx *context.APIContext) { // "$ref": "#/responses/notFound" workflowID := ctx.PathParam("workflow_id") - // Existing runs prove the workflow is/was valid and cover historical workflows - // whose file was later removed. Fall back to a git lookup for never-run workflows. + scopedWorkflowSourceRepoID := ctx.FormInt64("scoped_workflow_source_repo_id") + // Existing runs prove the workflow is/was valid and cover historical workflows whose file was later removed. + // Repo-level never-run workflows fall back to a git lookup; scoped workflows are selected by source repo ID and may return an empty run list. runExists, err := db.Exist[actions_model.ActionRun](ctx, actions_model.FindRunOptions{ - RepoID: ctx.Repo.Repository.ID, - WorkflowID: workflowID, + RepoID: ctx.Repo.Repository.ID, + WorkflowID: workflowID, + WorkflowRepoID: scopedWorkflowSourceRepoID, + IsScopedRun: optional.Some(scopedWorkflowSourceRepoID > 0), }.ToConds()) if err != nil { ctx.APIErrorInternal(err) return } if !runExists { - if _, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID); err != nil { - ctx.APIErrorAuto(err) - return + if scopedWorkflowSourceRepoID == 0 { + if _, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID); err != nil { + ctx.APIErrorAuto(err) + return + } } } @@ -1141,6 +1151,11 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) { // description: Whether the response should include the workflow run ID and URLs. // in: query // type: boolean + // - name: scoped_workflow_source_repo_id + // description: For a scoped workflow, the ID of the source repository providing it; omit or 0 for a repo-level workflow. + // in: query + // type: integer + // format: int64 // responses: // "200": // "$ref": "#/responses/RunDetails" @@ -1162,7 +1177,9 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) { return } - runID, err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error { + // a non-zero scoped_workflow_source_repo_id dispatches a scoped workflow from that source repo; 0/absent is repo-level. + scopedWorkflowSourceRepoID := ctx.FormInt64("scoped_workflow_source_repo_id") + runID, err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, scopedWorkflowSourceRepoID, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error { if strings.Contains(ctx.Req.Header.Get("Content-Type"), "form-urlencoded") { // The chi framework's "Binding" doesn't support to bind the form map values into a map[string]string // So we have to manually read the `inputs[key]` from the form diff --git a/routers/api/v1/repo/avatar.go b/routers/api/v1/repo/avatar.go index b00394ffcc..b28358fd21 100644 --- a/routers/api/v1/repo/avatar.go +++ b/routers/api/v1/repo/avatar.go @@ -13,7 +13,7 @@ import ( repo_service "gitea.dev/services/repository" ) -// UpdateVatar updates the Avatar of an Repo +// UpdateAvatar updates the Avatar of a Repo func UpdateAvatar(ctx *context.APIContext) { // swagger:operation POST /repos/{owner}/{repo}/avatar repository repoUpdateAvatar // --- @@ -56,7 +56,7 @@ func UpdateAvatar(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } -// UpdateAvatar deletes the Avatar of an Repo +// DeleteAvatar deletes the Avatar of a Repo func DeleteAvatar(ctx *context.APIContext) { // swagger:operation DELETE /repos/{owner}/{repo}/avatar repository repoDeleteAvatar // --- diff --git a/routers/api/v1/repo/blob.go b/routers/api/v1/repo/blob.go index 79b2dee245..d16cff0765 100644 --- a/routers/api/v1/repo/blob.go +++ b/routers/api/v1/repo/blob.go @@ -47,7 +47,7 @@ func GetBlob(ctx *context.APIContext) { return } - if blob, err := files_service.GetBlobBySHA(ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil { + if blob, err := files_service.GetBlobBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil { ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.JSON(http.StatusOK, blob) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 1b7b6f32b9..b890d52c7c 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -68,7 +68,7 @@ func GetBranch(ctx *context.APIContext) { return } - c, err := ctx.Repo.GitRepo.GetBranchCommit(branchName) + c, err := ctx.Repo.GitRepo.GetBranchCommit(ctx, branchName) if err != nil { ctx.APIErrorInternal(err) return @@ -219,14 +219,14 @@ func CreateBranch(ctx *context.APIContext) { var err error if len(opt.OldRefName) > 0 { - oldCommit, err = ctx.Repo.GitRepo.GetCommit(opt.OldRefName) + oldCommit, err = ctx.Repo.GitRepo.GetCommit(ctx, opt.OldRefName) if err != nil { ctx.APIErrorInternal(err) return } } else if len(opt.OldBranchName) > 0 { //nolint:staticcheck // deprecated field if exist, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, opt.OldBranchName); exist { //nolint:staticcheck // deprecated field - oldCommit, err = ctx.Repo.GitRepo.GetBranchCommit(opt.OldBranchName) //nolint:staticcheck // deprecated field + oldCommit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx, opt.OldBranchName) //nolint:staticcheck // deprecated field if err != nil { ctx.APIErrorInternal(err) return @@ -236,14 +236,14 @@ func CreateBranch(ctx *context.APIContext) { return } } else { - oldCommit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) + oldCommit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx, ctx.Repo.Repository.DefaultBranch) if err != nil { ctx.APIErrorInternal(err) return } } - err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, oldCommit.ID.String(), opt.BranchName) + err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, oldCommit.ID.String(), opt.BranchName) if err != nil { if git_model.IsErrBranchNotExist(err) { ctx.APIError(http.StatusNotFound, "The old branch does not exist") @@ -259,7 +259,7 @@ func CreateBranch(ctx *context.APIContext) { return } - commit, err := ctx.Repo.GitRepo.GetBranchCommit(opt.BranchName) + commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx, opt.BranchName) if err != nil { ctx.APIErrorInternal(err) return @@ -360,7 +360,7 @@ func ListBranches(ctx *context.APIContext) { apiBranches = make([]*api.Branch, 0, len(branches)) for i := range branches { - c, err := ctx.Repo.GitRepo.GetBranchCommit(branches[i].Name) + c, err := ctx.Repo.GitRepo.GetBranchCommit(ctx, branches[i].Name) if err != nil { // Skip if this branch doesn't exist anymore. if git.IsErrNotExist(err) { diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index 6f8aaefb6d..a445f79671 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -74,7 +74,7 @@ func GetSingleCommit(ctx *context.APIContext) { } func getCommit(ctx *context.APIContext, identifier string, toCommitOpts convert.ToCommitOptions) { - commit, err := ctx.Repo.GitRepo.GetCommit(identifier) + commit, err := ctx.Repo.GitRepo.GetCommit(ctx, identifier) if err != nil { if git.IsErrNotExist(err) { ctx.APIErrorNotFound("commit doesn't exist: " + identifier) @@ -208,14 +208,14 @@ func GetAllCommits(ctx *context.APIContext) { var baseCommit *git.Commit if len(sha) == 0 { // no sha supplied - use default branch - baseCommit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) + baseCommit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx, ctx.Repo.Repository.DefaultBranch) if err != nil { ctx.APIErrorInternal(err) return } } else { // get commit specified by sha - baseCommit, err = ctx.Repo.GitRepo.GetCommit(sha) + baseCommit, err = ctx.Repo.GitRepo.GetCommit(ctx, sha) if err != nil { ctx.APIErrorAuto(err) return @@ -235,7 +235,7 @@ func GetAllCommits(ctx *context.APIContext) { } // Query commits - commits, err = baseCommit.CommitsByRange(listOptions.Page, listOptions.PageSize, not, since, until) + commits, err = baseCommit.CommitsByRange(ctx, ctx.Repo.GitRepo, listOptions.Page, listOptions.PageSize, not, since, until) if err != nil { ctx.APIErrorInternal(err) return @@ -258,11 +258,30 @@ func GetAllCommits(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } else if commitsCountTotal == 0 { - ctx.APIErrorNotFound() - return + // when date filters are active, a zero count may just mean no + // commits in the requested range — not that the path is invalid + if since == "" && until == "" { + ctx.APIErrorNotFound() + return + } + // verify the path actually exists in the revision history + totalWithoutDate, err := gitrepo.CommitsCount(ctx, ctx.Repo.Repository, + gitrepo.CommitsCountOptions{ + Not: not, + Revision: []string{sha}, + RelPath: []string{path}, + }) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if totalWithoutDate == 0 { + ctx.APIErrorNotFound() + return + } } - commits, _, err = ctx.Repo.GitRepo.CommitsByFileAndRange( + commits, _, err = ctx.Repo.GitRepo.CommitsByFileAndRange(ctx, git.CommitsByFileAndRangeOptions{ Revision: sha, File: path, @@ -341,7 +360,7 @@ func DownloadCommitDiffOrPatch(ctx *context.APIContext) { sha := ctx.PathParam("sha") diffType := git.RawDiffType(ctx.PathParam("diffType")) - if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, diffType, ctx.Resp); err != nil { + if err := git.GetRawDiff(ctx, ctx.Repo.GitRepo, sha, diffType, ctx.Resp); err != nil { if git.IsErrNotExist(err) { ctx.APIErrorNotFound("commit doesn't exist: " + sha) return diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index ddd5a9bb21..afb4341787 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -38,7 +38,7 @@ func CompareDiff(ctx *context.APIContext) { // required: true // - name: basehead // in: path - // description: compare two refs as `base...head` (or `base..head`); refs may be branches, tags, full or short SHAs, including branch names that contain slashes. + // description: compare two refs as `base...head` (or `base..head`); refs may be branches, tags, full or short SHAs (including branch names that contain slashes), optionally with a `^` or `~N` revision suffix. // type: string // required: true // - name: output @@ -51,6 +51,8 @@ func CompareDiff(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/Compare" + // "400": + // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" @@ -118,9 +120,9 @@ func downloadCompareDiffOrPatch(ctx *context.APIContext, compareInfo *git_servic var err error if patch { - err = compareInfo.HeadGitRepo.GetPatch(compareArg, ctx.Resp) + err = compareInfo.HeadGitRepo.GetPatch(ctx, compareArg, ctx.Resp) } else { - err = compareInfo.HeadGitRepo.GetDiff(compareArg, ctx.Resp) + err = compareInfo.HeadGitRepo.GetDiff(ctx, compareArg, ctx.Resp) } if err != nil { ctx.APIErrorInternal(err) diff --git a/routers/api/v1/repo/download.go b/routers/api/v1/repo/download.go index 0df406488c..e88b19c00d 100644 --- a/routers/api/v1/repo/download.go +++ b/routers/api/v1/repo/download.go @@ -14,7 +14,7 @@ import ( ) func serveRepoArchive(ctx *context.APIContext, reqFileName string, paths []string) { - aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, reqFileName, paths) + aReq, err := archiver_service.NewRequest(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, reqFileName, paths) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.APIError(http.StatusBadRequest, err.Error()) diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 5f10c4fbd7..35e6f5ad2d 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -136,7 +136,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry))) // LFS Pointer files are at most 1024 bytes - so any blob greater than 1024 bytes cannot be an LFS file - if blob.Size() > lfs.MetaFileMaxSize { + if blob.Size(ctx) > lfs.MetaFileMaxSize { // First handle caching for the blob if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) { return @@ -151,7 +151,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { // OK, now the blob is known to have at most 1024 (lfs pointer max size) bytes, // we can simply read this in one go (This saves reading it twice) - lfsPointerBuf, err := blob.GetBlobBytes(lfs.MetaFileMaxSize) + lfsPointerBuf, err := blob.GetBlobBytes(ctx, lfs.MetaFileMaxSize) if err != nil { ctx.APIErrorInternal(err) return @@ -202,7 +202,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { } func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified *time.Time) { - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx, ctx.Repo.GitRepo, ctx.Repo.TreePath) if err != nil { if git.IsErrNotExist(err) { ctx.APIErrorNotFound() @@ -217,14 +217,14 @@ func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEn return nil, nil, nil } - latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath) + latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx, ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath) if err != nil { ctx.APIErrorInternal(err) return nil, nil, nil } when := &latestCommit.Committer.When - return entry.Blob(), entry, when + return entry.Blob(ctx.Repo.GitRepo), entry, when } // GetArchive get archive of a repository @@ -299,7 +299,7 @@ func GetEditorconfig(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - ec, _, err := ctx.Repo.GetEditorconfig(ctx.Repo.Commit) + ec, _, err := ctx.Repo.GetEditorconfig(ctx, ctx.Repo.Commit) if err != nil { ctx.APIErrorAuto(err) return diff --git a/routers/api/v1/repo/git_ref.go b/routers/api/v1/repo/git_ref.go index 6583e99e84..c6014e7311 100644 --- a/routers/api/v1/repo/git_ref.go +++ b/routers/api/v1/repo/git_ref.go @@ -14,7 +14,7 @@ import ( "gitea.dev/services/context" ) -// GetGitAllRefs get ref or an list all the refs of a repository +// GetGitAllRefs get ref or a list of all the refs of a repository func GetGitAllRefs(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/git/refs repository repoListAllGitRefs // --- diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go index c247fc0039..b3154696b7 100644 --- a/routers/api/v1/repo/hook.go +++ b/routers/api/v1/repo/hook.go @@ -177,7 +177,7 @@ func TestHook(ctx *context.APIContext) { commit := convert.ToPayloadCommit(ctx, ctx.Repo.Repository, ctx.Repo.Commit) commitID := ctx.Repo.Commit.ID.String() - if err := webhook_service.PrepareWebhook(ctx, hook, webhook_module.HookEventPush, &api.PushPayload{ + if err := webhook_service.PrepareTestWebhook(ctx, hook, webhook_module.HookEventPush, &api.PushPayload{ Ref: ref, Before: commitID, After: commitID, diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go index c1bbbe29d0..5f8f3cabae 100644 --- a/routers/api/v1/repo/issue_label.go +++ b/routers/api/v1/repo/issue_label.go @@ -178,13 +178,12 @@ func DeleteIssueLabel(ctx *context.APIContext) { return } - label, err := issues_model.GetLabelByID(ctx, ctx.PathParamInt64("id")) + // the label must belong to this repo (or its owning org); otherwise a foreign label ID + // is rejected the same way as a nonexistent one, closing a cross-repo enumeration oracle + labelID := ctx.PathParamInt64("id") + label, err := issues_model.GetLabelInRepoOrOrgByID(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner.ID, ctx.Repo.Owner.IsOrganization(), labelID) if err != nil { - if issues_model.IsErrLabelNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err.Error()) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index ff723e679f..58ff827ea9 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -9,6 +9,7 @@ import ( "gitea.dev/models/db" issues_model "gitea.dev/models/issues" + access_model "gitea.dev/models/perm/access" "gitea.dev/models/unit" user_model "gitea.dev/models/user" api "gitea.dev/modules/structs" @@ -132,11 +133,33 @@ func ListTrackedTimes(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } + trackedTimes, err = filterTrackedTimesByAccess(ctx, trackedTimes) + if err != nil { + ctx.APIErrorInternal(err) + return + } ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes)) } +func filterTrackedTimesByAccess(ctx *context.APIContext, trackedTimes issues_model.TrackedTimeList) (issues_model.TrackedTimeList, error) { + filtered := make(issues_model.TrackedTimeList, 0, len(trackedTimes)) + for _, trackedTime := range trackedTimes { + if trackedTime.Issue == nil || trackedTime.Issue.Repo == nil { + continue + } + permission, err := access_model.GetIndividualUserRepoPermission(ctx, trackedTime.Issue.Repo, ctx.Doer) + if err != nil { + return nil, err + } + if permission.HasAnyUnitAccessOrPublicAccess() { + filtered = append(filtered, trackedTime) + } + } + return filtered, nil +} + // AddTime add time manual to the given issue func AddTime(ctx *context.APIContext) { // swagger:operation Post /repos/{owner}/{repo}/issues/{index}/times issue issueAddTime @@ -198,7 +221,8 @@ func AddTime(ctx *context.APIContext) { // allow only RepoAdmin, Admin and User to add time user, err = user_model.GetUserByName(ctx, form.User) if err != nil { - ctx.APIErrorInternal(err) + ctx.APIErrorAuto(err) + return } } } @@ -542,6 +566,11 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } + trackedTimes, err = filterTrackedTimesByAccess(ctx, trackedTimes) + if err != nil { + ctx.APIErrorInternal(err) + return + } ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes)) @@ -604,6 +633,11 @@ func ListMyTrackedTimes(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } + trackedTimes, err = filterTrackedTimesByAccess(ctx, trackedTimes) + if err != nil { + ctx.APIErrorInternal(err) + return + } ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes)) diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go index 790a9212bc..cef0dfdfb1 100644 --- a/routers/api/v1/repo/label.go +++ b/routers/api/v1/repo/label.go @@ -106,11 +106,7 @@ func GetLabel(ctx *context.APIContext) { l, err = issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, intID) } if err != nil { - if issues_model.IsErrRepoLabelNotExist(err) { - ctx.APIErrorNotFound() - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -214,11 +210,7 @@ func EditLabel(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.EditLabelOption) l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) if err != nil { - if issues_model.IsErrRepoLabelNotExist(err) { - ctx.APIErrorNotFound() - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } diff --git a/routers/api/v1/repo/notes.go b/routers/api/v1/repo/notes.go index d2bd708aa4..8ec33e28f0 100644 --- a/routers/api/v1/repo/notes.go +++ b/routers/api/v1/repo/notes.go @@ -66,7 +66,7 @@ func getNote(ctx *context.APIContext, identifier string) { return } - commitID, err := ctx.Repo.GitRepo.ConvertToGitID(identifier) + commitID, err := ctx.Repo.GitRepo.ConvertToGitID(ctx, identifier) if err != nil { ctx.APIErrorAuto(err) return diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index d5c912d243..4a2ca01600 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -1087,12 +1087,6 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git baseRepo := ctx.Repo.Repository compareReq := common.ParseCompareRouterParam(compareParam) - // remove the check when we support compare with carets - if compareReq.BaseOriRefSuffix != "" { - ctx.APIError(http.StatusBadRequest, "Unsupported comparison syntax: ref with suffix") - return nil, nil - } - _, headRepo, err := common.GetHeadOwnerAndRepo(ctx, baseRepo, compareReq) switch { case errors.Is(err, util.ErrInvalidArgument): @@ -1113,7 +1107,7 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git headGitRepo = ctx.Repo.GitRepo closer = func() {} // no need to close the head repo because it shares the base repo } else { - headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo) + headGitRepo, err = gitrepo.OpenRepository(headRepo) if err != nil { ctx.APIErrorInternal(err) return nil, nil @@ -1152,10 +1146,18 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git return nil, nil } - baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx))) - headRef := headGitRepo.UnstableGuessRefByShortName(util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch)) + baseRef, err := common.ResolveRefWithSuffix(ctx, ctx.Repo.GitRepo, util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx)), compareReq.BaseOriRefSuffix) + if err != nil { + ctx.APIErrorAuto(err) + return nil, nil + } + headRef, err := common.ResolveRefWithSuffix(ctx, headGitRepo, util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch), compareReq.HeadOriRefSuffix) + if err != nil { + ctx.APIErrorAuto(err) + return nil, nil + } - log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.RelativePath(), compareReq.BaseOriRef, baseRef, compareReq.HeadOriRef, headRef) + log.Trace("Repo: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.FullName(), compareReq.BaseOriRef+compareReq.BaseOriRefSuffix, baseRef, compareReq.HeadOriRef+compareReq.HeadOriRefSuffix, headRef) baseRefValid := baseRef.IsBranch() || baseRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName), baseRef.ShortName()) headRefValid := headRef.IsBranch() || headRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(headRepo.ObjectFormatName), headRef.ShortName()) @@ -1249,6 +1251,13 @@ func UpdatePullRequest(ctx *context.APIContext) { return } + // a public-only token must not update (push into) a private head repo, + // even when the base repo named in the route is public + if !ctx.TokenCanAccessRepo(pr.HeadRepo) { + ctx.APIErrorNotFound() + return + } + // keep API back-compat: when no style is given, default to "merge" rather than the repo's DefaultUpdateStyle, // so existing API clients keep getting a merge update. rebase := repo_model.UpdateStyle(ctx.FormString("style", string(repo_model.UpdateStyleMerge))) == repo_model.UpdateStyleRebase @@ -1561,7 +1570,7 @@ func GetPullRequestFiles(ctx *context.APIContext) { return } - headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + headCommitID, err := baseGitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) if err != nil { ctx.APIErrorInternal(err) return diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 9778dc416a..0eb823f891 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -526,7 +526,7 @@ func CreatePullReview(ctx *context.APIContext) { } defer closer.Close() - headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + headCommitID, err := gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) if err != nil { ctx.APIErrorInternal(err) return @@ -645,7 +645,7 @@ func SubmitPullReview(ctx *context.APIContext) { return } - headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) if err != nil { ctx.APIErrorInternal(err) return diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index a4fc03ae32..12ad8a1014 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -269,7 +269,7 @@ func CreateRelease(ctx *context.APIContext) { } // GitHub doesn't have "tag_message", GitLab has: https://docs.gitlab.com/api/releases/#create-a-release // It doesn't need to be the same as the "release note" - if err := release_service.CreateRelease(ctx.Repo.GitRepo, rel, nil, form.TagMessage); err != nil { + if err := release_service.CreateRelease(ctx, ctx.Repo.GitRepo, rel, nil, form.TagMessage); err != nil { if repo_model.IsErrReleaseAlreadyExist(err) { ctx.APIError(http.StatusConflict, err.Error()) } else if release_service.IsErrProtectedTagName(err) { diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 8adef61000..531b48fe76 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -1200,7 +1200,7 @@ func GetIssueTemplates(ctx *context.APIContext) { // "$ref": "#/responses/IssueTemplates" // "404": // "$ref": "#/responses/notFound" - ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) + ret := issue.ParseTemplatesFromDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo) if cnt := len(ret.TemplateErrors); cnt != 0 { ctx.Resp.Header().Add("X-Gitea-Warning", "error occurs when parsing issue template: count="+strconv.Itoa(cnt)) } @@ -1230,7 +1230,7 @@ func GetIssueConfig(ctx *context.APIContext) { // "$ref": "#/responses/RepoIssueConfig" // "404": // "$ref": "#/responses/notFound" - issueConfig, _ := issue.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) + issueConfig, _ := issue.GetTemplateConfigFromDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo) ctx.JSON(http.StatusOK, issueConfig) } @@ -1257,7 +1257,7 @@ func ValidateIssueConfig(ctx *context.APIContext) { // "$ref": "#/responses/RepoIssueConfigValidation" // "404": // "$ref": "#/responses/notFound" - _, err := issue.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) + _, err := issue.GetTemplateConfigFromDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo) if err == nil { ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: true, Message: ""}) diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go index d43d5ea628..5acdc72e98 100644 --- a/routers/api/v1/repo/tag.go +++ b/routers/api/v1/repo/tag.go @@ -55,7 +55,7 @@ func ListTags(ctx *context.APIContext) { listOpts := utils.GetListOptions(ctx) - tags, total, err := ctx.Repo.GitRepo.GetTagInfos(listOpts.Page, listOpts.PageSize) + tags, total, err := ctx.Repo.GitRepo.GetTagInfos(ctx, listOpts.Page, listOpts.PageSize) if err != nil { ctx.APIErrorInternal(err) return @@ -107,13 +107,13 @@ func GetAnnotatedTag(ctx *context.APIContext) { return } - tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha) + tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(ctx, sha) if err != nil { ctx.APIError(http.StatusBadRequest, err.Error()) return } - commit, err := ctx.Repo.GitRepo.GetTagCommit(tag.Name) + commit, err := ctx.Repo.GitRepo.GetTagCommit(ctx, tag.Name) if err != nil { ctx.APIError(http.StatusBadRequest, err.Error()) return @@ -151,7 +151,7 @@ func GetTag(ctx *context.APIContext) { // "$ref": "#/responses/notFound" tagName := ctx.PathParam("*") - tag, err := ctx.Repo.GitRepo.GetTag(tagName) + tag, err := ctx.Repo.GitRepo.GetTag(ctx, tagName) if err != nil { ctx.APIErrorNotFound("tag doesn't exist: " + tagName) return @@ -201,7 +201,7 @@ func CreateTag(ctx *context.APIContext) { form.Target = ctx.Repo.Repository.DefaultBranch } - commit, err := ctx.Repo.GitRepo.GetCommit(form.Target) + commit, err := ctx.Repo.GitRepo.GetCommit(ctx, form.Target) if err != nil { ctx.APIError(http.StatusNotFound, fmt.Sprintf("target not found: %v", err)) return @@ -221,7 +221,7 @@ func CreateTag(ctx *context.APIContext) { return } - tag, err := ctx.Repo.GitRepo.GetTag(form.TagName) + tag, err := ctx.Repo.GitRepo.GetTag(ctx, form.TagName) if err != nil { ctx.APIErrorInternal(err) return diff --git a/routers/api/v1/repo/tree.go b/routers/api/v1/repo/tree.go index 7c41a0e86d..2514421ca6 100644 --- a/routers/api/v1/repo/tree.go +++ b/routers/api/v1/repo/tree.go @@ -62,7 +62,7 @@ func GetTree(ctx *context.APIContext) { return } if tree, err := files_service.GetTreeBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, sha, ctx.FormInt("page"), ctx.FormInt("per_page"), ctx.FormBool("recursive")); err != nil { - ctx.APIError(http.StatusBadRequest, err.Error()) + ctx.APIErrorAuto(err) } else { ctx.SetTotalCountHeader(int64(tree.TotalCount)) ctx.JSON(http.StatusOK, tree) diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index 67209fa127..3b9d90a6bf 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -177,17 +177,17 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi } // lookup filename in wiki - get filecontent, real filename - content, pageFilename := wikiContentsByName(ctx, commit, wikiName, false) + content, pageFilename := wikiContentsByName(ctx, wikiRepo, commit, wikiName, false) if ctx.Written() { return nil } - sidebarContent, _ := wikiContentsByName(ctx, commit, "_Sidebar", true) + sidebarContent, _ := wikiContentsByName(ctx, wikiRepo, commit, "_Sidebar", true) if ctx.Written() { return nil } - footerContent, _ := wikiContentsByName(ctx, commit, "_Footer", true) + footerContent, _ := wikiContentsByName(ctx, wikiRepo, commit, "_Footer", true) if ctx.Written() { return nil } @@ -196,7 +196,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename) // Get last change information. - lastCommit, err := wikiRepo.GetCommitByPath(pageFilename) + lastCommit, err := wikiRepo.GetCommitByPath(ctx, pageFilename) if err != nil { ctx.APIErrorInternal(err) return nil @@ -303,7 +303,7 @@ func ListWikiPages(ctx *context.APIContext) { skip := (page - 1) * limit maxNum := page * limit - entries, err := commit.ListEntries() + entries, err := commit.Tree().ListEntries(ctx, wikiRepo) if err != nil { ctx.APIErrorInternal(err) return @@ -313,7 +313,7 @@ func ListWikiPages(ctx *context.APIContext) { if i < skip || i >= maxNum || !entry.IsRegular() { continue } - c, err := wikiRepo.GetCommitByPath(entry.Name()) + c, err := wikiRepo.GetCommitByPath(ctx, entry.Name()) if err != nil { ctx.APIErrorInternal(err) return @@ -420,7 +420,7 @@ func ListPageRevisions(ctx *context.APIContext) { } // lookup filename in wiki - get filecontent, gitTree entry , real filename - _, pageFilename := wikiContentsByName(ctx, commit, pageName, false) + _, pageFilename := wikiContentsByName(ctx, wikiRepo, commit, pageName, false) if ctx.Written() { return } @@ -431,7 +431,7 @@ func ListPageRevisions(ctx *context.APIContext) { page := max(ctx.FormInt("page"), 1) // get Commit Count - commitsHistory, _, err := wikiRepo.CommitsByFileAndRange( + commitsHistory, _, err := wikiRepo.CommitsByFileAndRange(ctx, git.CommitsByFileAndRangeOptions{ Revision: ctx.Repo.Repository.DefaultWikiBranch, File: pageFilename, @@ -448,8 +448,8 @@ func ListPageRevisions(ctx *context.APIContext) { } // findEntryForFile finds the tree entry for a target filepath. -func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) { - entry, err := commit.GetTreeEntryByPath(target) +func findEntryForFile(ctx *context.APIContext, wikiRepo *git.Repository, commit *git.Commit, target string) (*git.TreeEntry, error) { + entry, err := commit.GetTreeEntryByPath(ctx, wikiRepo, target) if err != nil { return nil, err } @@ -462,19 +462,19 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) if unescapedTarget, err = url.QueryUnescape(target); err != nil { return nil, err } - return commit.GetTreeEntryByPath(unescapedTarget) + return commit.GetTreeEntryByPath(ctx, wikiRepo, unescapedTarget) } // findWikiRepoCommit opens the wiki repo and returns the latest commit, writing to context on error. // The caller is responsible for closing the returned repo again func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) { - wikiRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo()) + wikiRepo, err := gitrepo.OpenRepository(ctx.Repo.Repository.WikiStorageRepo()) if err != nil { ctx.APIErrorAuto(err) return nil, nil } - commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch) + commit, err := wikiRepo.GetBranchCommit(ctx, ctx.Repo.Repository.DefaultWikiBranch) if err != nil { ctx.APIErrorAuto(err) return wikiRepo, nil @@ -484,12 +484,12 @@ func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) // wikiContentsByEntry returns the contents of the wiki page referenced by the // given tree entry, encoded with base64. Writes to ctx if an error occurs. -func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string { - blob := entry.Blob() - if blob.Size() > setting.API.DefaultMaxBlobSize { +func wikiContentsByEntry(ctx *context.APIContext, wikiRepo *git.Repository, entry *git.TreeEntry) string { + blob := entry.Blob(wikiRepo) + if blob.Size(ctx) > setting.API.DefaultMaxBlobSize { return "" } - content, err := blob.GetBlobContentBase64(nil) + content, err := blob.GetBlobContentBase64(ctx, nil) if err != nil { ctx.APIErrorInternal(err) return "" @@ -499,9 +499,9 @@ func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string { // wikiContentsByName returns the contents of a wiki page, along with a boolean // indicating whether the page exists. Writes to ctx if an error occurs. -func wikiContentsByName(ctx *context.APIContext, commit *git.Commit, wikiName wiki_service.WebPath, isSidebarOrFooter bool) (string, string) { +func wikiContentsByName(ctx *context.APIContext, wikiRepo *git.Repository, commit *git.Commit, wikiName wiki_service.WebPath, isSidebarOrFooter bool) (string, string) { gitFilename := wiki_service.WebPathToGitPath(wikiName) - entry, err := findEntryForFile(commit, gitFilename) + entry, err := findEntryForFile(ctx, wikiRepo, commit, gitFilename) if err != nil { if git.IsErrNotExist(err) { if !isSidebarOrFooter { @@ -512,5 +512,5 @@ func wikiContentsByName(ctx *context.APIContext, commit *git.Commit, wikiName wi } return "", "" } - return wikiContentsByEntry(ctx, entry), gitFilename + return wikiContentsByEntry(ctx, wikiRepo, entry), gitFilename } diff --git a/routers/api/v1/shared/action.go b/routers/api/v1/shared/action.go index d62d0d3a22..58e47c0a98 100644 --- a/routers/api/v1/shared/action.go +++ b/routers/api/v1/shared/action.go @@ -21,8 +21,21 @@ import ( "gitea.dev/routers/api/v1/utils" "gitea.dev/services/context" "gitea.dev/services/convert" + + "xorm.io/builder" ) +// actionsOwnerAccessibleRepoIDsSubQuery returns the sub-query restricting an owner-scoped actions +// listing to the repos whose actions the caller can read, or nil when no restriction applies. A bare +// org member must not be able to enumerate runs/jobs of repos they have no access to. A site admin may +// skip the access filter, but a public-only token must stay confined to public repos even for an admin. +func actionsOwnerAccessibleRepoIDsSubQuery(ctx *context.APIContext, ownerID int64) *builder.Builder { + if ownerID > 0 && (ctx.Doer == nil || !ctx.Doer.IsAdmin || ctx.PublicOnly) { + return repo_model.FindUserActionsAccessibleOwnerRepoIDsSubQuery(ownerID, ctx.Doer, ctx.PublicOnly) + } + return nil +} + // ListJobs lists jobs for api route validated ownerID and repoID // ownerID == 0 and repoID == 0 means all jobs // ownerID == 0 and repoID != 0 means all jobs for the given repo @@ -60,6 +73,8 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64, runAttemptI opts.Statuses = append(opts.Statuses, values...) } + opts.AccessibleRepoIDsSubQuery = actionsOwnerAccessibleRepoIDsSubQuery(ctx, opts.OwnerID) + jobs, total, err := db.FindAndCount[actions_model.ActionRunJob](ctx, opts) if err != nil { ctx.APIErrorInternal(err) @@ -148,6 +163,11 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64, workflowID string) WorkflowID: workflowID, ListOptions: listOptions, } + if workflowID != "" { + workflowSourceRepoID := ctx.FormInt64("scoped_workflow_source_repo_id") + opts.IsScopedRun = optional.Some(workflowSourceRepoID > 0) + opts.WorkflowRepoID = workflowSourceRepoID + } if event := ctx.FormString("event"); event != "" { opts.TriggerEvent = webhook.HookEventType(event) @@ -176,6 +196,8 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64, workflowID string) } excludePullRequests := ctx.FormBool("exclude_pull_requests") + opts.AccessibleRepoIDsSubQuery = actionsOwnerAccessibleRepoIDsSubQuery(ctx, opts.OwnerID) + runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts) if err != nil { ctx.APIErrorInternal(err) diff --git a/routers/api/v1/swagger/action.go b/routers/api/v1/swagger/action.go index 559c561e9f..f8c865a0c0 100644 --- a/routers/api/v1/swagger/action.go +++ b/routers/api/v1/swagger/action.go @@ -12,13 +12,6 @@ type swaggerResponseSecretList struct { Body []api.Secret `json:"body"` } -// Secret -// swagger:response Secret -type swaggerResponseSecret struct { - // in:body - Body api.Secret `json:"body"` -} - // ActionVariable // swagger:response ActionVariable type swaggerResponseActionVariable struct { diff --git a/routers/api/v1/swagger/issue.go b/routers/api/v1/swagger/issue.go index ea8858a8e3..ca2abbed23 100644 --- a/routers/api/v1/swagger/issue.go +++ b/routers/api/v1/swagger/issue.go @@ -98,13 +98,6 @@ type swaggerIssueTemplates struct { Body []api.IssueTemplate `json:"body"` } -// StopWatch -// swagger:response StopWatch -type swaggerResponseStopWatch struct { - // in:body - Body api.StopWatch `json:"body"` -} - // StopWatchList // swagger:response StopWatchList type swaggerResponseStopWatchList struct { diff --git a/routers/api/v1/swagger/nodeinfo.go b/routers/api/v1/swagger/nodeinfo.go deleted file mode 100644 index f217b24bcb..0000000000 --- a/routers/api/v1/swagger/nodeinfo.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package swagger - -import ( - api "gitea.dev/modules/structs" -) - -// NodeInfo -// swagger:response NodeInfo -type swaggerResponseNodeInfo struct { - // in:body - Body api.NodeInfo `json:"body"` -} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index c234b8ec84..9a97d31eb6 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -8,11 +8,12 @@ import ( "gitea.dev/services/forms" ) -// not actually a response, just a hack to get go-swagger to include definitions -// of the various XYZOption structs +// not actually a set of parameters, just a hack to get go-swagger to include +// definitions of the various XYZOption structs. The annotation below uses an +// operation id that matches no route, so the spec carries no unused response +// or parameter for it while still emitting the referenced definitions. -// parameterBodies -// swagger:response parameterBodies +// swagger:parameters parameterBodies type swaggerParameterBodies struct { // in:body AddCollaboratorOption api.AddCollaboratorOption @@ -235,4 +236,7 @@ type swaggerParameterBodies struct { // in:body LockIssueOption api.LockIssueOption + + // in:body + MergeUpstreamRequest api.MergeUpstreamRequest } diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index e9a3a15821..42d1bb637a 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -84,13 +84,6 @@ type swaggerResponseTagProtection struct { Body api.TagProtection `json:"body"` } -// Reference -// swagger:response Reference -type swaggerResponseReference struct { - // in:body - Body api.Reference `json:"body"` -} - // ReferenceList // swagger:response ReferenceList type swaggerResponseReferenceList struct { @@ -511,12 +504,6 @@ type swaggerCompare struct { Body api.Compare `json:"body"` } -// swagger:response MergeUpstreamRequest -type swaggerMergeUpstreamRequest struct { - // in:body - Body api.MergeUpstreamRequest `json:"body"` -} - // swagger:response MergeUpstreamResponse type swaggerMergeUpstreamResponse struct { // in:body diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index 87aef1d10d..444740573e 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -126,6 +126,29 @@ func CreateAccessToken(ctx *context.APIContext) { } t.Scope = scope + // a token-authenticated request must not mint a token with a broader scope than its own + if ctx.Data["IsApiToken"] == true { + apiTokenScope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) + if !ok { + ctx.APIError(http.StatusForbidden, "the authenticating token has no scope") + return + } + hasScope, err := apiTokenScope.CanCreateChildScope(scope) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if !hasScope { + ctx.APIError(http.StatusForbidden, "cannot create an access token with a broader scope than the authenticating token") + return + } + // a public-only token must not mint a token that drops the public-only restriction + if t.Scope, err = t.Scope.EnforcePublicOnlyFrom(apiTokenScope); err != nil { + ctx.APIErrorInternal(err) + return + } + } + if err := auth_model.NewAccessToken(ctx, t); err != nil { ctx.APIErrorInternal(err) return diff --git a/routers/api/v1/user/email.go b/routers/api/v1/user/email.go index d3329f7a74..ea710b01dd 100644 --- a/routers/api/v1/user/email.go +++ b/routers/api/v1/user/email.go @@ -8,6 +8,7 @@ import ( "net/http" user_model "gitea.dev/models/user" + "gitea.dev/modules/setting" api "gitea.dev/modules/structs" "gitea.dev/modules/web" "gitea.dev/services/context" @@ -57,6 +58,11 @@ func AddEmail(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) { + ctx.APIErrorNotFound("emails are not allowed to be changed") + return + } + form := web.GetForm(ctx).(*api.CreateEmailOption) if len(form.Emails) == 0 { ctx.APIError(http.StatusUnprocessableEntity, "Email list empty") @@ -114,6 +120,11 @@ func DeleteEmail(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" + if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) { + ctx.APIErrorNotFound("emails are not allowed to be changed") + return + } + form := web.GetForm(ctx).(*api.DeleteEmailOption) if len(form.Emails) == 0 { ctx.Status(http.StatusNoContent) diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index c78b6872c5..146973021a 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -24,6 +24,7 @@ func getStarredRepos(ctx *context.APIContext, user *user_model.User, private boo ListOptions: utils.GetListOptions(ctx), StarrerID: user.ID, IncludePrivate: private, + Actor: user, } opts.ApplyPublicOnly(ctx.PublicOnly) @@ -32,13 +33,16 @@ func getStarredRepos(ctx *context.APIContext, user *user_model.User, private boo return nil, err } - repos := make([]*api.Repository, len(starredRepos)) - for i, starred := range starredRepos { + repos := make([]*api.Repository, 0, len(starredRepos)) + for _, starred := range starredRepos { permission, err := access_model.GetIndividualUserRepoPermission(ctx, starred, user) if err != nil { return nil, err } - repos[i] = convert.ToRepo(ctx, starred, permission) + if !permission.HasAnyUnitAccessOrPublicAccess() { + continue + } + repos = append(repos, convert.ToRepo(ctx, starred, permission)) } return repos, nil } diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go index 2d1f55d870..e0396efea0 100644 --- a/routers/api/v1/user/watch.go +++ b/routers/api/v1/user/watch.go @@ -22,6 +22,7 @@ func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private boo ListOptions: utils.GetListOptions(ctx), WatcherID: user.ID, IncludePrivate: private, + Actor: user, } opts.ApplyPublicOnly(ctx.PublicOnly) diff --git a/routers/api/v1/utils/git.go b/routers/api/v1/utils/git.go index 0123073d17..b88e8051d6 100644 --- a/routers/api/v1/utils/git.go +++ b/routers/api/v1/utils/git.go @@ -38,7 +38,7 @@ func ResolveRefCommit(ctx reqctx.RequestContext, repo *repo_model.Repository, in if refCommit.RefName == "" { return nil, git.ErrNotExist{ID: inputRef} } - if refCommit.Commit, err = gitRepo.GetCommit(refCommit.RefName.String()); err != nil { + if refCommit.Commit, err = gitRepo.GetCommit(ctx, refCommit.RefName.String()); err != nil { return nil, err } refCommit.CommitID = refCommit.Commit.ID.String() @@ -57,6 +57,6 @@ func GetGitRefs(ctx *context.APIContext, filter string) ([]*git.Reference, strin if len(filter) > 0 { filter = "refs/" + filter } - refs, err := ctx.Repo.GitRepo.GetRefsFiltered(filter) + refs, err := ctx.Repo.GitRepo.GetRefsFiltered(ctx, filter) return refs, "GetRefsFiltered", err } diff --git a/routers/common/compare.go b/routers/common/compare.go index 667a32a965..4bbe27d6ca 100644 --- a/routers/common/compare.go +++ b/routers/common/compare.go @@ -5,7 +5,9 @@ package common import ( "context" + "regexp" "strings" + "sync" repo_model "gitea.dev/models/repo" user_model "gitea.dev/models/user" @@ -19,9 +21,10 @@ type CompareRouterReq struct { CompareSeparator string - HeadOwner string - HeadRepoName string - HeadOriRef string + HeadOwner string + HeadRepoName string + HeadOriRef string + HeadOriRefSuffix string } func (cr *CompareRouterReq) DirectComparison() bool { @@ -79,9 +82,11 @@ func ParseCompareRouterParam(routerParam string) *CompareRouterReq { sep = ".." basePart, headPart, ok = strings.Cut(routerParam, sep) if !ok { - headOwnerName, headRepoName, headRef := parseHead(routerParam) + headOwnerName, headRepoName, headOriRef := parseHead(routerParam) + headOriRef, headOriRefSuffix := git.ParseRefSuffix(headOriRef) return &CompareRouterReq{ - HeadOriRef: headRef, + HeadOriRef: headOriRef, + HeadOriRefSuffix: headOriRefSuffix, HeadOwner: headOwnerName, HeadRepoName: headRepoName, CompareSeparator: "...", @@ -92,9 +97,36 @@ func ParseCompareRouterParam(routerParam string) *CompareRouterReq { ci := &CompareRouterReq{CompareSeparator: sep} ci.BaseOriRef, ci.BaseOriRefSuffix = git.ParseRefSuffix(basePart) ci.HeadOwner, ci.HeadRepoName, ci.HeadOriRef = parseHead(headPart) + ci.HeadOriRef, ci.HeadOriRefSuffix = git.ParseRefSuffix(ci.HeadOriRef) return ci } +// validRefSuffix matches only ^/~ ancestry navigation. The ^{...}, @{...} and :path forms address +// other objects (trees, blobs) or reflog/upstream state that compare does not resolve, so they are rejected. +var validRefSuffix = sync.OnceValue(func() *regexp.Regexp { + return regexp.MustCompile(`^(?:[~^][0-9]*)+$`) +}) + +// ResolveRefWithSuffix resolves oriRef plus an optional revision suffix (^, ~N) to a RefName. +// A nil error guarantees a usable RefName: an unsupported suffix yields an invalid-argument error +// and an unresolvable ref yields a not-found error. +func ResolveRefWithSuffix(ctx context.Context, gitRepo *git.Repository, oriRef, refSuffix string) (git.RefName, error) { + if refSuffix == "" { + if refName := gitRepo.UnstableGuessRefByShortName(ctx, oriRef); refName != "" { + return refName, nil + } + return "", util.NewNotExistErrorf("ref %q does not exist", oriRef) + } + if !validRefSuffix().MatchString(refSuffix) { + return "", util.NewInvalidArgumentErrorf("unsupported ref suffix %q", refSuffix) + } + commit, err := gitRepo.GetCommit(ctx, oriRef+refSuffix) + if err != nil { + return "", util.NewNotExistErrorf("ref %q does not exist", oriRef+refSuffix) + } + return git.RefNameFromCommit(commit.ID.String()), nil +} + // maxForkTraverseLevel defines the maximum levels to traverse when searching for the head repository. const maxForkTraverseLevel = 10 diff --git a/routers/common/compare_test.go b/routers/common/compare_test.go index e4e24a03cf..1c1f66be50 100644 --- a/routers/common/compare_test.go +++ b/routers/common/compare_test.go @@ -6,6 +6,8 @@ package common import ( "testing" + "gitea.dev/modules/util" + "github.com/stretchr/testify/assert" ) @@ -97,9 +99,56 @@ func TestCompareRouterReq(t *testing.T) { HeadOriRef: "develop", }, }, + { + input: "main...develop^", + CompareRouterReq: &CompareRouterReq{ + BaseOriRef: "main", + CompareSeparator: "...", + HeadOriRef: "develop", + HeadOriRefSuffix: "^", + }, + }, + { + input: "main~2...develop", + CompareRouterReq: &CompareRouterReq{ + BaseOriRef: "main", + BaseOriRefSuffix: "~2", + CompareSeparator: "...", + HeadOriRef: "develop", + }, + }, + { + input: "main...lunny/forked_repo:develop~3", + CompareRouterReq: &CompareRouterReq{ + BaseOriRef: "main", + CompareSeparator: "...", + HeadOwner: "lunny", + HeadRepoName: "forked_repo", + HeadOriRef: "develop", + HeadOriRefSuffix: "~3", + }, + }, + { + input: "develop^", + CompareRouterReq: &CompareRouterReq{ + CompareSeparator: "...", + HeadOriRef: "develop", + HeadOriRefSuffix: "^", + }, + }, } for _, c := range cases { assert.Equal(t, c.CompareRouterReq, ParseCompareRouterParam(c.input), "input: %s", c.input) } } + +func TestResolveRefWithSuffix(t *testing.T) { + // The ^{...}, @{...} and :path forms address non-commit objects or reflog state, so they are + // rejected before any repository access and a nil repo is fine here. + for _, refSuffix := range []string{"^{/Add}", "^{commit}", "@{upstream}", "~1:path"} { + ref, err := ResolveRefWithSuffix(t.Context(), nil, "branch", refSuffix) + assert.ErrorIs(t, err, util.ErrInvalidArgument, "suffix %q", refSuffix) + assert.Empty(t, ref, "suffix %q", refSuffix) + } +} diff --git a/routers/common/middleware.go b/routers/common/middleware.go index 603e18e7bd..b750e85ad8 100644 --- a/routers/common/middleware.go +++ b/routers/common/middleware.go @@ -147,6 +147,9 @@ func MustInitSessioner() func(next http.Handler) http.Handler { Secure: setting.SessionConfig.Secure, SameSite: setting.SessionConfig.SameSite, Domain: setting.SessionConfig.Domain, + + // in the future, if websocket is used, the websocket handler should manage its own session sync (release) + IgnoreReleaseForWebSocket: true, }) if err != nil { log.Fatal("common.Sessioner failed: %v", err) diff --git a/routers/common/serve.go b/routers/common/serve.go index 5bb3f5316a..740742a890 100644 --- a/routers/common/serve.go +++ b/routers/common/serve.go @@ -26,7 +26,7 @@ func ServeBlob(ctx *context.Base, repo *repo_model.Repository, filePath string, return err } - dataRc, err := blob.DataAsync() + dataRc, err := blob.DataAsync(ctx) if err != nil { return err } @@ -35,7 +35,7 @@ func ServeBlob(ctx *context.Base, repo *repo_model.Repository, filePath string, if lastModified == nil { lastModified = new(time.Time) } - httplib.ServeUserContentByReader(ctx.Req, ctx.Resp, blob.Size(), dataRc, httplib.ServeHeaderOptions{ + httplib.ServeUserContentByReader(ctx.Req, ctx.Resp, blob.Size(ctx), dataRc, httplib.ServeHeaderOptions{ Filename: path.Base(filePath), CacheIsPublic: !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePublic, CacheDuration: setting.StaticCacheTime, diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index e19bee3e7d..a809f2813c 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -63,7 +63,7 @@ func hookPostReceiveSyncDatabaseBranches(ctx *gitea_context.PrivateContext, opts } if update.IsDelRef() { if err := git_model.MarkBranchAsDeleted(ctx, repo.ID, update.RefFullName.BranchName(), update.PusherID); err != nil { - ctx.PrivateError(http.StatusInternalServerError, err, fmt.Sprintf("failed to mark branch %s as deleted", update.RefFullName)) + ctx.PrivateInternalErrorf("failed to mark branch %s as deleted: %v", update.RefFullName, err) return false } } else { @@ -79,7 +79,7 @@ func hookPostReceiveSyncDatabaseBranches(ctx *gitea_context.PrivateContext, opts gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo) if err != nil { - ctx.PrivateError(http.StatusInternalServerError, err, "failed to open repository") + ctx.PrivateInternalErrorf("failed to open repository: %v", err) return false } @@ -90,8 +90,8 @@ func hookPostReceiveSyncDatabaseBranches(ctx *gitea_context.PrivateContext, opts commitIDs = append(commitIDs, update.NewCommitID) } - if err = repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, branchNames, commitIDs, gitRepo.GetCommit); err != nil { - ctx.PrivateError(http.StatusInternalServerError, err, "failed to sync branch to DB") + if err = repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, gitRepo, branchNames, commitIDs); err != nil { + ctx.PrivateInternalErrorf("failed to sync branch to DB: %v", err) return false } return true @@ -133,7 +133,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { // push async updates if err := repo_service.PushUpdates(updates...); err != nil { - ctx.PrivateError(http.StatusInternalServerError, err, "failed to push updates") + ctx.PrivateInternalErrorf("failed to push updates: %v", err) return } @@ -147,23 +147,30 @@ func hookPostReceiveUpdateRepoByOptions(ctx *gitea_context.PrivateContext, opts if isPrivate.Has() || isTemplate.Has() { pusher, err := loadContextCacheUser(ctx, opts.UserID) if err != nil { - ctx.PrivateError(http.StatusInternalServerError, err, "failed to load pusher user") + ctx.PrivateInternalErrorf("failed to load pusher user: %v", err) return false } perm, err := access_model.GetDoerRepoPermission(ctx, repo, pusher) if err != nil { - ctx.PrivateError(http.StatusInternalServerError, err, "failed to load doer repo permission") + ctx.PrivateInternalErrorf("failed to load doer repo permission: %v", err) return false } if !perm.IsOwner() && !perm.IsAdmin() { - ctx.PrivateError(http.StatusNotFound, nil, "permission denied") + ctx.PrivateUserErrorf(http.StatusNotFound, "permission denied") return false } - // FIXME: these options are not quite right, for example: changing visibility should do more works than just setting the is_private flag - // These options should only be used for "push-to-create" + // Only honor these options while the repo is still empty (the push-to-create + // case). On a populated repo a bare "git push -o repo.private=..." would + // silently flip visibility, bypassing the audit log, webhooks and notifications. + if !repo.IsEmpty { + return true + } + + // The repo is empty and being initialized by this push, so there is no + // dependent state (webhooks, notifications, visibility fan-out) to reconcile + // yet; setting the flags directly is sufficient in this push-to-create case. if isPrivate.Has() && repo.IsPrivate != isPrivate.Value() { - // TODO: it needs to do more work repo.IsPrivate = isPrivate.Value() if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil { log.Error("failed to update repo is_private: %v", err) @@ -184,7 +191,7 @@ func hookPostReceiveRespondWithTrailer(ctx *gitea_context.PrivateContext, opts * baseRepo := repo if repo.IsFork { if err := repo.GetBaseRepo(ctx); err != nil { - ctx.PrivateError(http.StatusInternalServerError, err, "failed to load base repo") + ctx.PrivateInternalErrorf("failed to load base repo: %v", err) return } if repo.BaseRepo.AllowsPulls(ctx) { @@ -215,7 +222,7 @@ func hookPostReceiveRespondWithTrailer(ctx *gitea_context.PrivateContext, opts * pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, issues_model.PullRequestFlowGithub) if err != nil && !errors.Is(err, util.ErrNotExist) { - ctx.PrivateError(http.StatusInternalServerError, err, "failed to get active PR for branch "+branch) + ctx.PrivateInternalErrorf("failed to get active PR for branch %s: %v", branch, err) return } if pr == nil { @@ -245,20 +252,19 @@ func loadContextCacheUser(ctx context.Context, id int64) (*user_model.User, erro // hookPostReceiveHandlePullRequestMerging handle pull request merging, a pull request action should push at least 1 commit func hookPostReceiveHandlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.HookOptions, updates []*repo_module.PushUpdateOptions) bool { if len(updates) == 0 { - err := fmt.Errorf("Pushing a merged PR (pr:%d) no commits pushed ", opts.PullRequestID) - ctx.PrivateError(http.StatusInternalServerError, err, "no push update") + ctx.PrivateInternalErrorf("Pushing a merged PR (pr:%d) no commits pushed ", opts.PullRequestID) return false } pr, err := issues_model.GetPullRequestByID(ctx, opts.PullRequestID) if err != nil { - ctx.PrivateError(http.StatusInternalServerError, err, "failed to load pull request") + ctx.PrivateInternalErrorf("failed to get pull request %d: %v", opts.PullRequestID, err) return false } pusher, err := loadContextCacheUser(ctx, opts.UserID) if err != nil { - ctx.PrivateError(http.StatusInternalServerError, err, "failed to load pusher user") + ctx.PrivateInternalErrorf("failed to load pusher user %d: %v", opts.UserID, err) return false } @@ -266,7 +272,7 @@ func hookPostReceiveHandlePullRequestMerging(ctx *gitea_context.PrivateContext, // here to keep it as before, that maybe PullRequestStatusMergeable _, err = pull_service.SetMerged(ctx, pr, updates[len(updates)-1].NewCommitID, timeutil.TimeStampNow(), pusher, pr.Status) if err != nil { - ctx.PrivateError(http.StatusInternalServerError, err, "failed to set pr to merged") + ctx.PrivateInternalErrorf("failed to set pr %d to merged: %v", pr.ID, err) return false } return true @@ -289,10 +295,10 @@ func hookPostReceiveSyncRepoDefaultBranch(ctx *gitea_context.PrivateContext, opt } // if default branch doesn't exist, try to guess one from existing git repo - _, err = gitRepo.GetBranchCommitID(repo.DefaultBranch) + _, err = gitRepo.GetBranchCommitID(ctx, repo.DefaultBranch) if errors.Is(err, util.ErrNotExist) { for _, guessBranchName := range []string{"main", "master"} { - if _, err = gitRepo.GetBranchCommitID(guessBranchName); err == nil { + if _, err = gitRepo.GetBranchCommitID(ctx, guessBranchName); err == nil { repo.DefaultBranch = guessBranchName err = repo_model.UpdateDefaultBranch(ctx, repo) if err != nil { diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index 39836ab016..2036ff1421 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -31,9 +31,7 @@ import ( type preReceiveContext struct { *gitea_context.PrivateContext - // loadedPusher indicates that where the following information are loaded - loadedPusher bool - user *user_model.User // it's the org user if a DeployKey is used + user *user_model.User // the "pusher", it's the org user if a DeployKey is used userPerm access_model.Permission deployKeyAccessMode perm_model.AccessMode @@ -53,10 +51,7 @@ type preReceiveContext struct { func (ctx *preReceiveContext) canWriteCodeUnit() bool { if ctx.canWriteCodeUnitCached == nil { - var canWrite bool - if ctx.loadPusherAndPermission() { - canWrite = ctx.userPerm.CanWrite(unit.TypeCode) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite - } + canWrite := ctx.userPerm.CanWrite(unit.TypeCode) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite ctx.canWriteCodeUnitCached = &canWrite } return *ctx.canWriteCodeUnitCached @@ -91,9 +86,6 @@ func (ctx *preReceiveContext) assertCanWriteRef(refFullName git.RefName) bool { // CanCreatePullRequest returns true if pusher can create pull requests func (ctx *preReceiveContext) CanCreatePullRequest() bool { if !ctx.checkedCanCreatePullRequest { - if !ctx.loadPusherAndPermission() { - return false - } ctx.canCreatePullRequest = ctx.userPerm.CanRead(unit.TypePullRequests) ctx.checkedCanCreatePullRequest = true } @@ -124,6 +116,10 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { opts: opts, } + if !ourCtx.loadPusherAndPermission() { + return // if error occurs, loadPusherAndPermission had written the error response + } + // Iterate across the provided old commit IDs for i := range opts.OldCommitIDs { oldCommitID := opts.OldCommitIDs[i] @@ -230,7 +226,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r // 3. Enforce require signed commits if protectBranch.RequireSignedCommits { - err := verifyCommits(oldCommitID, newCommitID, gitRepo, ctx.env) + err := verifyCommits(ctx, oldCommitID, newCommitID, gitRepo, ctx.env) if err != nil { if !isErrUnverifiedCommit(err) { log.Error("Unable to check commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) @@ -256,7 +252,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r globs := protectBranch.GetProtectedFilePatterns() if len(globs) > 0 { - _, err := pull_service.CheckFileProtection(gitRepo, branchName, oldCommitID, newCommitID, globs, 1, ctx.env) + _, err := pull_service.CheckFileProtection(ctx, gitRepo, branchName, oldCommitID, newCommitID, globs, 1, ctx.env) if err != nil { if !pull_service.IsErrFilePathProtected(err) { log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) @@ -281,18 +277,10 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys) } } else { - user, err := user_model.GetUserByID(ctx, ctx.opts.UserID) - if err != nil { - log.Error("Unable to GetUserByID for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Unable to GetUserByID for commits from %s to %s: %v", oldCommitID, newCommitID, err), - }) - return - } if isForcePush { - canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, user) + canPush = !changedProtectedfiles && protectBranch.CanUserForcePush(ctx, ctx.user) } else { - canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, user) + canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, ctx.user) } } @@ -314,7 +302,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r // Allow commits that only touch unprotected files globs := protectBranch.GetUnprotectedFilePatterns() if len(globs) > 0 { - unprotectedFilesOnly, err := pull_service.CheckUnprotectedFiles(gitRepo, branchName, oldCommitID, newCommitID, globs, ctx.env) + unprotectedFilesOnly, err := pull_service.CheckUnprotectedFiles(ctx, gitRepo, branchName, oldCommitID, newCommitID, globs, ctx.env) if err != nil { log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) ctx.JSON(http.StatusInternalServerError, private.Response{ @@ -354,12 +342,6 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r return } - // although we should have called `loadPusherAndPermission` before, here we call it explicitly again because we need to access ctx.user below - if !ctx.loadPusherAndPermission() { - // if error occurs, loadPusherAndPermission had written the error response - return - } - // Now check if the user is allowed to merge PRs for this repository // Note: we can use ctx.perm and ctx.user directly as they will have been loaded above allowedMerge, err := pull_service.IsUserAllowedToMerge(ctx, pr, ctx.userPerm, ctx.user) @@ -499,10 +481,6 @@ func generateGitEnv(opts *private.HookOptions) (env []string) { // loadPusherAndPermission returns false if an error occurs, and it writes the error response func (ctx *preReceiveContext) loadPusherAndPermission() bool { - if ctx.loadedPusher { - return true - } - if ctx.opts.UserID == user_model.ActionsUserID { taskID := ctx.opts.ActionsTaskID ctx.user = user_model.NewActionsUserWithTaskID(taskID) @@ -555,7 +533,5 @@ func (ctx *preReceiveContext) loadPusherAndPermission() bool { } ctx.deployKeyAccessMode = deployKey.Mode } - - ctx.loadedPusher = true return true } diff --git a/routers/private/hook_pre_receive_test.go b/routers/private/hook_pre_receive_test.go index bea5e8974d..3c1c21673f 100644 --- a/routers/private/hook_pre_receive_test.go +++ b/routers/private/hook_pre_receive_test.go @@ -52,7 +52,6 @@ func TestPreReceiveCanWriteCodePerBranch(t *testing.T) { mockCtx, _ := contexttest.MockPrivateContext(t, "/") ctx := &preReceiveContext{ PrivateContext: mockCtx, - loadedPusher: true, user: maintainer, userPerm: headPerm, } diff --git a/routers/private/hook_verification.go b/routers/private/hook_verification.go index a21c12d465..9621f6ff66 100644 --- a/routers/private/hook_verification.go +++ b/routers/private/hook_verification.go @@ -5,6 +5,7 @@ package private import ( "bufio" + "context" "io" "gitea.dev/modules/git" @@ -15,9 +16,9 @@ import ( // This file contains commit verification functions for refs passed across in hooks -func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []string) error { +func verifyCommits(ctx context.Context, oldCommitID, newCommitID string, repo *git.Repository, env []string) error { var command *gitcmd.Command - objectFormat, _ := repo.GetObjectFormat() + objectFormat, _ := repo.GetObjectFormat(ctx) if oldCommitID == objectFormat.EmptyObjectID().String() { // When creating a new branch, the oldCommitID is empty, by using "newCommitID --not --all": // List commits that are reachable by following the newCommitID, exclude "all" existing heads/tags commits @@ -32,22 +33,22 @@ func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env [] err := command.WithEnv(env). WithDir(repo.Path). - WithPipelineFunc(func(ctx gitcmd.Context) error { - err := readAndVerifyCommitsFromShaReader(stdoutReader, repo, env) - return ctx.CancelPipeline(err) + WithPipelineFunc(func(gitCtx gitcmd.Context) error { + err := readAndVerifyCommitsFromShaReader(ctx, stdoutReader, repo, env) + return gitCtx.CancelPipeline(err) }). - Run(repo.Ctx) + Run(ctx) if err != nil && !isErrUnverifiedCommit(err) { log.Error("Unable to check commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err) } return err } -func readAndVerifyCommitsFromShaReader(input io.ReadCloser, repo *git.Repository, env []string) error { +func readAndVerifyCommitsFromShaReader(ctx context.Context, input io.ReadCloser, repo *git.Repository, env []string) error { scanner := bufio.NewScanner(input) for scanner.Scan() { line := scanner.Text() - err := readAndVerifyCommit(line, repo, env) + err := readAndVerifyCommit(ctx, line, repo, env) if err != nil { return err } @@ -55,7 +56,7 @@ func readAndVerifyCommitsFromShaReader(input io.ReadCloser, repo *git.Repository return scanner.Err() } -func readAndVerifyCommit(sha string, repo *git.Repository, env []string) error { +func readAndVerifyCommit(ctx context.Context, sha string, repo *git.Repository, env []string) error { commitID := git.MustIDFromString(sha) cmd := gitcmd.NewCommand("cat-file", "commit").AddDynamicArguments(sha) stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe() @@ -63,18 +64,18 @@ func readAndVerifyCommit(sha string, repo *git.Repository, env []string) error { return cmd.WithEnv(env). WithDir(repo.Path). - WithPipelineFunc(func(ctx gitcmd.Context) error { - commit, err := git.CommitFromReader(repo, commitID, stdoutReader) + WithPipelineFunc(func(gitCtx gitcmd.Context) error { + commit, err := git.CommitFromReader(commitID, stdoutReader) if err != nil { return err } verification := asymkey_service.ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return ctx.CancelPipeline(&errUnverifiedCommit{commit.ID.String()}) + return gitCtx.CancelPipeline(&errUnverifiedCommit{commit.ID.String()}) } return nil }). - Run(repo.Ctx) + Run(ctx) } type errUnverifiedCommit struct { diff --git a/routers/private/hook_verification_test.go b/routers/private/hook_verification_test.go index 30b309e917..18131b9075 100644 --- a/routers/private/hook_verification_test.go +++ b/routers/private/hook_verification_test.go @@ -10,20 +10,17 @@ import ( "gitea.dev/modules/git" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -var testReposDir = "tests/repos/" - func TestVerifyCommits(t *testing.T) { unittest.PrepareTestEnv(t) - gitRepo, err := git.OpenRepository(t.Context(), testReposDir+"repo1_hook_verification") - if err != nil { - defer gitRepo.Close() - } - assert.NoError(t, err) + gitRepo, err := git.OpenRepositoryLocal("tests/repos/repo1_hook_verification") + require.NoError(t, err) + defer gitRepo.Close() - objectFormat, err := gitRepo.GetObjectFormat() + objectFormat, err := gitRepo.GetObjectFormat(t.Context()) assert.NoError(t, err) testCases := []struct { @@ -37,7 +34,7 @@ func TestVerifyCommits(t *testing.T) { } for _, tc := range testCases { - err = verifyCommits(tc.base, tc.head, gitRepo, nil) + err = verifyCommits(t.Context(), tc.base, tc.head, gitRepo, nil) if tc.verified { assert.NoError(t, err) } else { diff --git a/routers/private/serv.go b/routers/private/serv.go index 7c7d0db5a6..b9fa76ce85 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -4,7 +4,6 @@ package private import ( - "fmt" "net/http" "strings" @@ -15,9 +14,11 @@ import ( "gitea.dev/models/unit" user_model "gitea.dev/models/user" "gitea.dev/modules/git" + "gitea.dev/modules/git/gitcmd" "gitea.dev/modules/log" "gitea.dev/modules/private" "gitea.dev/modules/setting" + "gitea.dev/modules/util" "gitea.dev/services/context" repo_service "gitea.dev/services/repository" wiki_service "gitea.dev/services/wiki" @@ -27,24 +28,18 @@ import ( func ServNoCommand(ctx *context.PrivateContext) { keyID := ctx.PathParamInt64("keyid") if keyID <= 0 { - ctx.JSON(http.StatusBadRequest, private.Response{ - UserMsg: fmt.Sprintf("Bad key id: %d", keyID), - }) + ctx.PrivateUserErrorf(http.StatusBadRequest, "Bad key id: %d", keyID) + return } results := private.KeyAndOwner{} key, err := asymkey_model.GetPublicKeyByID(ctx, keyID) if err != nil { if asymkey_model.IsErrKeyNotExist(err) { - ctx.JSON(http.StatusUnauthorized, private.Response{ - UserMsg: fmt.Sprintf("Cannot find key: %d", keyID), - }) + ctx.PrivateUserErrorf(http.StatusUnauthorized, "Cannot find key: %d", keyID) return } - log.Error("Unable to get public key: %d Error: %v", keyID, err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: err.Error(), - }) + ctx.PrivateInternalErrorf("Unable to get public key: %d Error: %v", keyID, err) return } results.Key = key @@ -53,21 +48,14 @@ func ServNoCommand(ctx *context.PrivateContext) { user, err := user_model.GetUserByID(ctx, key.OwnerID) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.JSON(http.StatusUnauthorized, private.Response{ - UserMsg: fmt.Sprintf("Cannot find owner with id: %d for key: %d", key.OwnerID, keyID), - }) + ctx.PrivateUserErrorf(http.StatusUnauthorized, "Cannot find owner with id: %d for key: %d", key.OwnerID, keyID) return } - log.Error("Unable to get owner with id: %d for public key: %d Error: %v", key.OwnerID, keyID, err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: err.Error(), - }) + ctx.PrivateInternalErrorf("Unable to get owner with id: %d for public key: %d Error: %v", key.OwnerID, keyID, err) return } if !user.IsActive || user.ProhibitLogin { - ctx.JSON(http.StatusForbidden, private.Response{ - UserMsg: "Your account is disabled.", - }) + ctx.PrivateUserErrorf(http.StatusForbidden, "Your account is disabled.") return } results.Owner = user @@ -78,87 +66,56 @@ func ServNoCommand(ctx *context.PrivateContext) { // ServCommand returns information about the provided keyid func ServCommand(ctx *context.PrivateContext) { keyID := ctx.PathParamInt64("keyid") - ownerName := ctx.PathParam("owner") - repoName := ctx.PathParam("repo") + reqOwnerName := ctx.PathParam("owner") + reqRepoName := ctx.PathParam("repo") mode := perm.AccessMode(ctx.FormInt("mode")) verb := ctx.FormString("verb") // Set the basic parts of the results to return results := private.ServCommandResults{ - RepoName: repoName, - OwnerName: ownerName, + OwnerName: reqOwnerName, // it might be changed if there is "renamed user redirection" + RepoName: reqRepoName, // it might be changed if there is "renamed repo redirection", or the repo is a wiki KeyID: keyID, } + repoLogName := reqOwnerName + "/" + reqRepoName - // Now because we're not translating things properly let's just default some English strings here - modeString := "read" - if mode > perm.AccessModeRead { - modeString = "write to" - } - - // The default unit we're trying to look at is code - unitType := unit.TypeCode - - // Unless we're a wiki... - if strings.HasSuffix(repoName, ".wiki") { - // in which case we need to look at the wiki - unitType = unit.TypeWiki - // And we'd better munge the reponame and tell downstream we're looking at a wiki + if reqWikiRepoName, ok := strings.CutSuffix(reqRepoName, ".wiki"); ok { + // in which case we need to look at the wiki, trim the ".wiki" suffix, only use the main repo name results.IsWiki = true - results.RepoName = repoName[:len(repoName)-5] + results.RepoName = reqWikiRepoName } + unitType := util.Iif(results.IsWiki, unit.TypeWiki, unit.TypeCode) + modeString := mode.ToString() owner, err := user_model.GetUserByName(ctx, results.OwnerName) if err != nil { if !user_model.IsErrUserNotExist(err) { - log.Error("Unable to get repository owner: %s/%s Error: %v", results.OwnerName, results.RepoName, err) - ctx.JSON(http.StatusForbidden, private.Response{ - UserMsg: fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err), - }) + ctx.PrivateInternalErrorf("Unable to get repository owner for %s, error: %v", repoLogName, err) return } // Check if there is a user redirect for the requested owner redirectedUserID, err := user_model.LookupUserRedirect(ctx, results.OwnerName) if err != nil { - // User is fetching/cloning a non-existent repository - log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr()) - ctx.JSON(http.StatusNotFound, private.Response{ - UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), - }) + ctx.PrivateUserErrorf(http.StatusNotFound, "Cannot find repository %s", repoLogName) return } - redirectUser, err := user_model.GetUserByID(ctx, redirectedUserID) if err != nil { - // User is fetching/cloning a non-existent repository - log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr()) - ctx.JSON(http.StatusNotFound, private.Response{ - UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), - }) + ctx.PrivateUserErrorf(http.StatusNotFound, "Cannot find repository: %s", repoLogName) return } - log.Info("User %s has been redirected to %s", results.OwnerName, redirectUser.Name) + log.Debug("User %s has been redirected to %s", results.OwnerName, redirectUser.Name) results.OwnerName = redirectUser.Name owner = redirectUser } - if !owner.IsOrganization() && !owner.IsActive { - ctx.JSON(http.StatusForbidden, private.Response{ - UserMsg: "Repository cannot be accessed, you could retry it later", - }) - return - } // Now get the Repository and set the results section - repoExist := true repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, results.RepoName) if err != nil { if !repo_model.IsErrRepoNotExist(err) { - log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err), - }) + ctx.PrivateInternalErrorf("Unable to get repository %s, error: %v", repoLogName, err) return } @@ -166,53 +123,53 @@ func ServCommand(ctx *context.PrivateContext) { if err == nil { redirectedRepo, err := repo_model.GetRepositoryByID(ctx, redirectedRepoID) if err == nil { - log.Info("Repository %s/%s has been redirected to %s/%s", results.OwnerName, results.RepoName, redirectedRepo.OwnerName, redirectedRepo.Name) + log.Info("Repository %s has been redirected to %s/%s", repoLogName, redirectedRepo.OwnerName, redirectedRepo.Name) + repo = redirectedRepo + if err = repo.LoadOwner(ctx); err != nil { + ctx.PrivateInternalErrorf("Unable to repository owner %d", repo.OwnerID) + return + } + owner = repo.Owner results.RepoName = redirectedRepo.Name results.OwnerName = redirectedRepo.OwnerName - repo = redirectedRepo - owner.ID = redirectedRepo.OwnerID + repoLogName = results.OwnerName + "/" + results.RepoName + util.Iif(results.IsWiki, ".wiki", "") } else { - log.Warn("Repo %s/%s has a redirect to repo with ID %d, but no repo with this ID could be found. Trying without redirect...", results.OwnerName, results.RepoName, redirectedRepoID) + log.Warn("Repo %s has a redirect to repo with ID %d, but no repo with this ID could be found. Trying without redirect...", repoLogName, redirectedRepoID) } } if repo == nil { - repoExist = false if mode == perm.AccessModeRead { // User is fetching/cloning a non-existent repository - log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr()) - ctx.JSON(http.StatusNotFound, private.Response{ - UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), - }) + ctx.PrivateUserErrorf(http.StatusNotFound, "Cannot find repository %s", repoLogName) return } } } - if repoExist { + if !owner.IsOrganization() && !owner.IsActive { + ctx.PrivateUserErrorf(http.StatusForbidden, "Repository cannot be accessed, the owner is inactive.") + return + } + + if repo != nil { repo.Owner = owner - repo.OwnerName = ownerName + repo.OwnerName = owner.Name results.RepoID = repo.ID if repo.IsBeingCreated() { - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: "Repository is being created, you could retry after it finished", - }) + ctx.PrivateUserErrorf(http.StatusForbidden, "Repository is being created, you could retry after it finished") return } if repo.IsBroken() { - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: "Repository is in a broken state", - }) + ctx.PrivateUserErrorf(http.StatusForbidden, "Repository is in a broken state") return } // We can shortcut at this point if the repo is a mirror if mode > perm.AccessModeRead && repo.IsMirror { - ctx.JSON(http.StatusForbidden, private.Response{ - UserMsg: fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName), - }) + ctx.PrivateUserErrorf(http.StatusForbidden, "Mirror Repository %s is read-only", repoLogName) return } } @@ -221,48 +178,33 @@ func ServCommand(ctx *context.PrivateContext) { key, err := asymkey_model.GetPublicKeyByID(ctx, keyID) if err != nil { if asymkey_model.IsErrKeyNotExist(err) { - ctx.JSON(http.StatusNotFound, private.Response{ - UserMsg: fmt.Sprintf("Cannot find key: %d", keyID), - }) + ctx.PrivateUserErrorf(http.StatusNotFound, "Cannot find key: %d", keyID) return } - log.Error("Unable to get public key: %d Error: %v", keyID, err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Unable to get key: %d Error: %v", keyID, err), - }) + ctx.PrivateInternalErrorf("Unable to get key: %d, error: %v", keyID, err) return } results.KeyName = key.Name results.KeyID = key.ID results.UserID = key.OwnerID - // If repo doesn't exist, deploy key doesn't make sense - if !repoExist && key.Type == asymkey_model.KeyTypeDeploy { - ctx.JSON(http.StatusNotFound, private.Response{ - UserMsg: fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName), - }) - return - } - // Deploy Keys have ownerID set to 0 therefore we can't use the owner // So now we need to check if the key is a deploy key // We'll keep hold of the deploy key here for permissions checking var deployKey *asymkey_model.DeployKey var user *user_model.User if key.Type == asymkey_model.KeyTypeDeploy { - var err error + if repo == nil { + ctx.PrivateUserErrorf(http.StatusNotFound, "Cannot find repository %s", repoLogName) + return + } deployKey, err = asymkey_model.GetDeployKeyByRepo(ctx, key.ID, repo.ID) if err != nil { if asymkey_model.IsErrDeployKeyNotExist(err) { - ctx.JSON(http.StatusNotFound, private.Response{ - UserMsg: fmt.Sprintf("Public (Deploy) Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), - }) + ctx.PrivateUserErrorf(http.StatusNotFound, "Deploy key %d:%s has no %q permission for %s.", key.ID, key.Name, modeString, repoLogName) return } - log.Error("Unable to get deploy for public (deploy) key: %d in %-v Error: %v", key.ID, repo, err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Unable to get Deploy Key for Public Key: %d:%s in %s/%s.", key.ID, key.Name, results.OwnerName, results.RepoName), - }) + ctx.PrivateInternalErrorf("Unable to get deploy for public (deploy) key %d for %s, error: %v", key.ID, repoLogName, err) return } results.DeployKeyID = deployKey.ID @@ -278,26 +220,18 @@ func ServCommand(ctx *context.PrivateContext) { } } else { // Get the user represented by the Key - var err error user, err = user_model.GetUserByID(ctx, key.OwnerID) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.JSON(http.StatusUnauthorized, private.Response{ - UserMsg: fmt.Sprintf("Public Key: %d:%s owner %d does not exist.", key.ID, key.Name, key.OwnerID), - }) + ctx.PrivateUserErrorf(http.StatusUnauthorized, "Public key %d:%s owner %d does not exist.", key.ID, key.Name, key.OwnerID) return } - log.Error("Unable to get owner: %d for public key: %d:%s Error: %v", key.OwnerID, key.ID, key.Name, err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Unable to get Owner: %d for Deploy Key: %d:%s in %s/%s.", key.OwnerID, key.ID, key.Name, ownerName, repoName), - }) + ctx.PrivateInternalErrorf("Unable to get key owner %d for public key %d:%s, error: %v", key.OwnerID, key.ID, key.Name, err) return } if !user.IsActive || user.ProhibitLogin { - ctx.JSON(http.StatusForbidden, private.Response{ - UserMsg: "Your account is disabled.", - }) + ctx.PrivateUserErrorf(http.StatusForbidden, "Your account is disabled.") return } @@ -308,25 +242,21 @@ func ServCommand(ctx *context.PrivateContext) { } // Don't allow pushing if the repo is archived - if repoExist && mode > perm.AccessModeRead && repo.IsArchived { - ctx.JSON(http.StatusUnauthorized, private.Response{ - UserMsg: fmt.Sprintf("Repo: %s/%s is archived.", results.OwnerName, results.RepoName), - }) + if repo != nil && mode > perm.AccessModeRead && repo.IsArchived { + ctx.PrivateUserErrorf(http.StatusUnauthorized, "Repo %s is archived.", repoLogName) return } // Permissions checking: - if repoExist && + if repo != nil && (mode > perm.AccessModeRead || repo.IsPrivate || owner.Visibility.IsPrivate() || - (user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey + (user != nil && user.IsRestricted) || // user will be nil if the key is a deploy key setting.Service.RequireSignInViewStrict) { if key.Type == asymkey_model.KeyTypeDeploy { - if deployKey.Mode < mode { - ctx.JSON(http.StatusUnauthorized, private.Response{ - UserMsg: fmt.Sprintf("Deploy Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), - }) + if deployKey == nil || deployKey.Mode < mode { + ctx.PrivateUserErrorf(http.StatusUnauthorized, "Deploy key %d:%s has no %q permission for %s.", key.ID, key.Name, modeString, repoLogName) return } } else { @@ -338,56 +268,34 @@ func ServCommand(ctx *context.PrivateContext) { mode = perm.AccessModeRead } - perm, err := access_model.GetDoerRepoPermission(ctx, repo, user) + userPerm, err := access_model.GetDoerRepoPermission(ctx, repo, user) if err != nil { - log.Error("Unable to get permissions for %-v with key %d in %-v Error: %v", user, key.ID, repo, err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Unable to get permissions for user %d:%s with key %d in %s/%s Error: %v", user.ID, user.Name, key.ID, results.OwnerName, results.RepoName, err), - }) + ctx.PrivateInternalErrorf("Unable to get permissions for %-v with key %d in %-v, error: %v", user, key.ID, repo, err) return } - userMode := perm.UnitAccessMode(unitType) - + userMode := userPerm.UnitAccessMode(unitType) if userMode < mode { - log.Warn("Failed authentication attempt for %s with key %s (not authorized to %s %s/%s) from %s", user.Name, key.Name, modeString, ownerName, repoName, ctx.RemoteAddr()) - ctx.JSON(http.StatusUnauthorized, private.Response{ - UserMsg: fmt.Sprintf("User: %d:%s with Key: %d:%s is not authorized to %s %s/%s.", user.ID, user.Name, key.ID, key.Name, modeString, ownerName, repoName), - }) + ctx.PrivateUserErrorf(http.StatusUnauthorized, "User %d with key %d:%s has no %q permission for %s", key.OwnerID, key.ID, key.Name, modeString, repoLogName) return } } } // We already know we aren't using a deploy key - if !repoExist { - owner, err := user_model.GetUserByName(ctx, ownerName) - if err != nil { - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Unable to get owner: %s %v", results.OwnerName, err), - }) - return - } - + if repo == nil { if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg { - ctx.JSON(http.StatusForbidden, private.Response{ - UserMsg: "Push to create is not enabled for organizations.", - }) + ctx.PrivateUserErrorf(http.StatusForbidden, "Push to create is not enabled for organizations.") return } if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser { - ctx.JSON(http.StatusForbidden, private.Response{ - UserMsg: "Push to create is not enabled for users.", - }) + ctx.PrivateUserErrorf(http.StatusForbidden, "Push to create is not enabled for users.") return } repo, err = repo_service.PushCreateRepo(ctx, user, owner, results.RepoName) if err != nil { - log.Error("pushCreateRepo: %v", err) - ctx.JSON(http.StatusNotFound, private.Response{ - UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), - }) + ctx.PrivateInternalErrorf("pushCreateRepo: %v", err) return } results.RepoID = repo.ID @@ -397,38 +305,23 @@ func ServCommand(ctx *context.PrivateContext) { // Ensure the wiki is enabled before we allow access to it if _, err := repo.GetUnit(ctx, unit.TypeWiki); err != nil { if repo_model.IsErrUnitTypeNotExist(err) { - ctx.JSON(http.StatusForbidden, private.Response{ - UserMsg: "repository wiki is disabled", - }) + ctx.PrivateUserErrorf(http.StatusForbidden, "repository wiki is disabled") return } - log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Failed to get the wiki unit in %s/%s Error: %v", ownerName, repoName, err), - }) + ctx.PrivateInternalErrorf("Failed to get the wiki unit in %-v, error: %v", repo, err) return } // Finally if we're trying to touch the wiki we should init it if err = wiki_service.InitWiki(ctx, repo); err != nil { - log.Error("Failed to initialize the wiki in %-v Error: %v", repo, err) - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Failed to initialize the wiki in %s/%s Error: %v", ownerName, repoName, err), - }) + ctx.PrivateInternalErrorf("Failed to initialize the wiki in %-v, error: %v", repo, err) return } } - log.Debug("Serv Results:\nIsWiki: %t\nDeployKeyID: %d\nKeyID: %d\tKeyName: %s\nUserName: %s\nUserID: %d\nOwnerName: %s\nRepoName: %s\nRepoID: %d", - results.IsWiki, - results.DeployKeyID, - results.KeyID, - results.KeyName, - results.UserName, - results.UserID, - results.OwnerName, - results.RepoName, - results.RepoID) + gitRepo := util.Iif(results.IsWiki, repo.WikiStorageRepo(), repo.CodeStorageRepo()) + results.RepoStoragePath = gitcmd.RepoLocalPath(gitRepo) + log.Debug("Serv Results: %+v", results) ctx.JSON(http.StatusOK, results) // We will update the keys in a different call. } diff --git a/routers/web/admin/queue_tester.go b/routers/web/admin/queue_tester.go index be7cb687e4..e3f04ac705 100644 --- a/routers/web/admin/queue_tester.go +++ b/routers/web/admin/queue_tester.go @@ -56,6 +56,7 @@ func initTestQueueOnce() { for { select { case <-ctx.Done(): + return case <-time.After(500 * time.Millisecond): if adding { if testQueue.GetQueueItemNumber() == qs.Length { diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go index 4f5e3ed842..af64106a85 100644 --- a/routers/web/admin/repos.go +++ b/routers/web/admin/repos.go @@ -134,7 +134,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) { ctx.ServerError("IsRepositoryExist", err) return } - exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName))) + exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.CodeRepoByName(ctxUser.Name, repoName)) if err != nil { ctx.ServerError("IsDir", err) return diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index f918c8b5d3..e5844350aa 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -450,27 +450,9 @@ func EditUserPost(ctx *context.Context) { log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, u.Name) if form.Reset2FA { - tf, err := auth.GetTwoFactorByUID(ctx, u.ID) - if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - ctx.ServerError("auth.GetTwoFactorByUID", err) + if _, _, err := auth.DisableTwoFactor(ctx, u.ID); err != nil { + ctx.ServerError("auth.DisableTwoFactor", err) return - } else if tf != nil { - if err := auth.DeleteTwoFactorByID(ctx, tf.ID, u.ID); err != nil { - ctx.ServerError("auth.DeleteTwoFactorByID", err) - return - } - } - - wn, err := auth.GetWebAuthnCredentialsByUID(ctx, u.ID) - if err != nil { - ctx.ServerError("auth.GetTwoFactorByUID", err) - return - } - for _, cred := range wn { - if _, err := auth.DeleteCredential(ctx, cred.ID, u.ID); err != nil { - ctx.ServerError("auth.DeleteCredential", err) - return - } } } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 63d069d31d..4c8a379ad0 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -118,7 +118,7 @@ func autoSignIn(ctx *context.Context) (bool, error) { ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day) - if err := updateSession(ctx, nil, map[string]any{ + if err := regenerateSession(ctx, nil, map[string]any{ session.KeyUID: u.ID, session.KeyUname: u.Name, session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth, @@ -357,7 +357,7 @@ func SignInPost(ctx *context.Context) { // User will need to use WebAuthn, save data updates["totpEnrolled"] = u.ID } - if err := updateSession(ctx, nil, updates); err != nil { + if err := regenerateSession(ctx, nil, updates); err != nil { ctx.ServerError("UserSignIn: Unable to update session", err) return } @@ -398,7 +398,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember bool) { return } - if err := updateSession(ctx, []string{ + if err := regenerateSession(ctx, []string{ // Delete the openid, 2fa and link_account data "openid_verified_uri", "openid_signin_remember", @@ -627,14 +627,19 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, if possibleLinkAccountData != nil && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) { switch setting.OAuth2Client.AccountLinking { case setting.OAuth2AccountLinkingAuto: - var user *user_model.User - user = &user_model.User{Name: u.Name} - hasUser, err := user_model.GetIndividualUser(ctx, user) - if !hasUser || err != nil { - user = &user_model.User{Email: u.Email} - hasUser, err = user_model.GetIndividualUser(ctx, user) - if !hasUser || err != nil { - ctx.ServerError("UserLinkAccount", err) + user, err := user_model.GetIndividualUserByName(ctx, u.Name) + if err != nil { + if !user_model.IsErrUserNotExist(err) { + ctx.ServerError("GetIndividualUserByName", err) + return false + } + user, err = user_model.GetIndividualUserByPrimaryEmail(ctx, u.Email) + if err != nil { + if !user_model.IsErrUserNotExist(err) { + ctx.ServerError("GetIndividualUserByPrimaryEmail", err) + } else { + ctx.NotFound(err) + } return false } } @@ -879,7 +884,7 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) { log.Trace("User activated: %s", user.Name) - if err := updateSession(ctx, nil, map[string]any{ + if err := regenerateSession(ctx, nil, map[string]any{ "uid": user.ID, "uname": user.Name, }); err != nil { @@ -931,7 +936,7 @@ func ActivateEmail(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + "/user/settings/account") } -func updateSession(ctx *context.Context, deletes []string, updates map[string]any) error { +func regenerateSession(ctx *context.Context, deletes []string, updates map[string]any) error { if _, err := session.RegenerateSession(ctx.Resp, ctx.Req); err != nil { return fmt.Errorf("regenerate session: %w", err) } diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index 1885cc5fdf..e2adac1bdd 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -164,7 +164,12 @@ func oauth2LinkAccount(ctx *context.Context, u *user_model.User, linkAccountData return } - if err := updateSession(ctx, nil, map[string]any{ + if err := Oauth2SetLinkAccountData(ctx, *linkAccountData); err != nil { + ctx.ServerError("Oauth2SetLinkAccountData", err) + return + } + + if err := regenerateSession(ctx, nil, map[string]any{ // User needs to use 2FA, save data and redirect to 2FA page. "twofaUid": u.ID, "twofaRemember": remember, diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 06c1b20d57..e861ed9637 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -19,9 +19,11 @@ import ( user_model "gitea.dev/models/user" auth_module "gitea.dev/modules/auth" "gitea.dev/modules/container" + "gitea.dev/modules/hostmatcher" "gitea.dev/modules/httplib" "gitea.dev/modules/log" "gitea.dev/modules/optional" + "gitea.dev/modules/proxy" "gitea.dev/modules/session" "gitea.dev/modules/setting" source_service "gitea.dev/services/auth/source" @@ -285,9 +287,7 @@ func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData { } func Oauth2SetLinkAccountData(ctx *context.Context, linkAccountData LinkAccountData) error { - return updateSession(ctx, nil, map[string]any{ - "linkAccountData": linkAccountData, - }) + return ctx.Session.Set("linkAccountData", linkAccountData) } func showLinkingLogin(ctx *context.Context, authSourceID int64, gothUser goth.User) { @@ -298,7 +298,23 @@ func showLinkingLogin(ctx *context.Context, authSourceID int64, gothUser goth.Us ctx.Redirect(setting.AppSubURL + "/user/link_account") } -var oauth2AvatarHTTPClient = &http.Client{Timeout: 30 * time.Second} +// oauth2AvatarAllowList parses the host allow-list applied to avatar fetches from the global +// [security] ALLOWED_HOST_LIST, defaulting an empty setting to the built-in "external" set. An empty +// host-match list would otherwise disable the allow-list check entirely and permit any host, including +// loopback/private addresses (SSRF). +func oauth2AvatarAllowList() *hostmatcher.HostMatchList { + return hostmatcher.ParseHostMatchList("security.ALLOWED_HOST_LIST", setting.Security.AllowedHostList) +} + +// oauth2AvatarHTTPClient builds the SSRF-protected client for avatar fetches. It is constructed per call +// so a changed allowlist takes effect (avatar fetches are infrequent, so this is not a hot path). +func oauth2AvatarHTTPClient() *http.Client { + allowList := oauth2AvatarAllowList() + return &http.Client{ + Timeout: 30 * time.Second, + Transport: hostmatcher.NewHTTPTransport("oauth2-avatar", allowList, nil, proxy.Proxy(), setting.Proxy.ProxyURLFixed, nil), + } +} func oauth2UpdateAvatarIfNeed(ctx *context.Context, avatarURL string, u *user_model.User) { if !setting.OAuth2Client.UpdateAvatar || len(avatarURL) == 0 { @@ -312,7 +328,7 @@ func oauth2UpdateAvatarIfNeed(ctx *context.Context, avatarURL string, u *user_mo // Some hosts (e.g. Wikimedia) reject Go's default User-Agent. req.Header.Set("User-Agent", "Gitea "+setting.AppVer) - resp, err := oauth2AvatarHTTPClient.Do(req) + resp, err := oauth2AvatarHTTPClient().Do(req) if err != nil { log.Warn("fetch %q failed: %v", avatarURL, err) return @@ -375,7 +391,10 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m ctx.ServerError("GetExternalLogin", err) return } - isDisabledByAutoSync := hasExt && extLogin.RefreshToken == "" + // the cron clears all three token fields when it disables a user, so require the + // full signature; a RefreshToken alone is empty for many normal logins (e.g. GitHub + // or OIDC without offline_access), which would otherwise reactivate admin-disabled users + isDisabledByAutoSync := hasExt && extLogin.AccessToken == "" && extLogin.RefreshToken == "" && extLogin.ExpiresAt.IsZero() if isDisabledByAutoSync { opts.IsActive = optional.Some(true) } @@ -409,7 +428,7 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m return } - if err := updateSession(ctx, nil, map[string]any{ + if err := regenerateSession(ctx, nil, map[string]any{ session.KeyUID: u.ID, session.KeyUname: u.Name, session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth, @@ -434,7 +453,7 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m } } - if err := updateSession(ctx, nil, map[string]any{ + if err := regenerateSession(ctx, nil, map[string]any{ // User needs to use 2FA, save data and redirect to 2FA page. "twofaUid": u.ID, "twofaRemember": false, @@ -503,13 +522,7 @@ func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, requ } } - user := &user_model.User{ - LoginName: gothUser.UserID, - LoginType: auth.OAuth2, - LoginSource: authSource.ID, - } - - hasUser, err := user_model.GetIndividualUser(ctx, user) + user, hasUser, err := user_model.GetIndividualUserByLoginSource(ctx, auth.OAuth2, authSource.ID, gothUser.UserID) if err != nil { return nil, goth.User{}, err } diff --git a/routers/web/auth/oauth2_provider.go b/routers/web/auth/oauth2_provider.go index 0ed20329b4..7b473ed0ec 100644 --- a/routers/web/auth/oauth2_provider.go +++ b/routers/web/auth/oauth2_provider.go @@ -98,6 +98,19 @@ func InfoOAuth(ctx *context.Context) { return } + // enforce the same user scope the REST API requires before returning identity + // claims; OIDC access tokens map to the "all" scope, so standard OIDC clients + // are unaffected and only explicitly-restricted tokens are rejected + tokenScope, _ := ctx.Data["ApiTokenScope"].(auth.AccessTokenScope) + if allowed, err := tokenScope.HasScope(auth.AccessTokenScopeReadUser); err != nil { + ctx.ServerError("HasScope", err) + return + } else if !allowed { + ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm="Gitea OAuth2"`) + ctx.PlainText(http.StatusForbidden, "token does not have required scope: read:user") + return + } + response := &userInfoResponse{ Sub: strconv.FormatInt(ctx.Doer.ID, 10), Name: ctx.Doer.DisplayName(), @@ -128,7 +141,7 @@ func InfoOAuth(ctx *context.Context) { // IntrospectOAuth introspects an oauth token func IntrospectOAuth(ctx *context.Context) { - clientIDValid := false + var introspectingApp *auth.OAuth2Application authHeader := ctx.Req.Header.Get("Authorization") if parsed, ok := httpauth.ParseAuthorizationHeader(authHeader); ok && parsed.BasicAuth != nil { clientID, clientSecret := parsed.BasicAuth.Username, parsed.BasicAuth.Password @@ -139,9 +152,14 @@ func IntrospectOAuth(ctx *context.Context) { ctx.HTTPError(http.StatusInternalServerError) return } - clientIDValid = err == nil && app.ValidateClientSecret([]byte(clientSecret)) + clientIDValid := err == nil && app.ValidateClientSecret([]byte(clientSecret)) + if clientIDValid { + introspectingApp = app + } } - if !clientIDValid { + if introspectingApp == nil { + // RFC 7662 requires the caller to authenticate to the introspection endpoint. + // https://www.rfc-editor.org/rfc/rfc7662.html#section-2.1 ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea OAuth2"`) ctx.PlainText(http.StatusUnauthorized, "no valid authorization") return @@ -156,21 +174,36 @@ func IntrospectOAuth(ctx *context.Context) { form := web.GetForm(ctx).(*forms.IntrospectTokenForm) token, err := oauth2_provider.ParseToken(form.Token, oauth2_provider.DefaultSigningKey) - if err == nil { - grant, err := auth.GetOAuth2GrantByID(ctx, token.GrantID) - if err == nil && grant != nil { - app, err := auth.GetOAuth2ApplicationByID(ctx, grant.ApplicationID) - if err == nil && app != nil { - response.Active = true - response.Scope = grant.Scope - response.RegisteredClaims = oauth2_provider.NewJwtRegisteredClaimsFromUser(app.ClientID, grant.UserID, nil /*exp*/) - } - if user, err := user_model.GetUserByID(ctx, grant.UserID); err == nil { - response.Username = user.Name - } - } + if err != nil { + // RFC 7662 returns inactive token metadata for invalid/unknown tokens. + // https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2 + log.Trace("Ignoring invalid token during introspection: %v", err) + ctx.JSON(http.StatusOK, response) + return } + grant, err := auth.GetOAuth2GrantByID(ctx, token.GrantID) + if err != nil { + ctx.ServerError("GetOAuth2GrantByID", err) + return + } + if grant == nil || grant.ApplicationID != introspectingApp.ID { + // RFC 7662 allows the server to reply inactive when the caller must not learn more. + // https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2 + ctx.JSON(http.StatusOK, response) + return + } + + response.Active = true + response.Scope = grant.Scope + response.RegisteredClaims = oauth2_provider.NewJwtRegisteredClaimsFromUser(introspectingApp.ClientID, grant.UserID, nil /*exp*/) + user, err := user_model.GetUserByID(ctx, grant.UserID) + if err != nil { + ctx.ServerError("GetUserByID", err) + return + } + response.Username = user.Name + ctx.JSON(http.StatusOK, response) } diff --git a/routers/web/auth/oauth_test.go b/routers/web/auth/oauth_test.go index e605f00680..36e4445ad8 100644 --- a/routers/web/auth/oauth_test.go +++ b/routers/web/auth/oauth_test.go @@ -4,15 +4,22 @@ package auth import ( + "net/http" + "net/http/httptest" + "sync/atomic" "testing" "gitea.dev/models/auth" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" + "gitea.dev/modules/hostmatcher" + "gitea.dev/modules/setting" + "gitea.dev/modules/test" "gitea.dev/services/oauth2_provider" "github.com/golang-jwt/jwt/v5" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func createAndParseToken(t *testing.T, grant *auth.OAuth2Grant) *oauth2_provider.OIDCToken { @@ -73,3 +80,42 @@ func TestNewAccessTokenResponse_OIDCToken(t *testing.T) { assert.Equal(t, user.Email, oidcToken.Email) assert.Equal(t, user.IsActive, oidcToken.EmailVerified) } + +func TestOAuth2AvatarClientBlocksLoopback(t *testing.T) { + var hit atomic.Bool + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hit.Store(true) + _, _ = w.Write([]byte("img")) + })) + defer srv.Close() + + // the httptest server binds a loopback address, which the SSRF-protected dialer must refuse + resp, err := oauth2AvatarHTTPClient().Get(srv.URL) + if resp != nil { + _ = resp.Body.Close() + } + require.Error(t, err) + assert.False(t, hit.Load(), "avatar client must refuse to dial a loopback address") +} + +func TestOAuth2AvatarAllowListRestricts(t *testing.T) { + defer test.MockVariableValue(&setting.Security.AllowedHostList, "avatars.example.com")() + allowList := oauth2AvatarAllowList() + assert.True(t, allowList.MatchHostName("avatars.example.com"), "the configured host must be allowed") + assert.False(t, allowList.MatchHostName("8.8.8.8"), "an unrelated external host must be rejected") + + // the default `external` allow-list still permits external hosts + setting.Security.AllowedHostList = hostmatcher.MatchBuiltinExternal + assert.True(t, oauth2AvatarAllowList().MatchHostName("8.8.8.8"), "default allow-list permits external hosts") +} + +func TestOAuth2AvatarClientBlocksCloudMetadata(t *testing.T) { + // external-only allow-list must reject link-local cloud metadata (169.254.169.254) at dial time + resp, err := oauth2AvatarHTTPClient().Get("http://169.254.169.254/latest/meta-data/") + if resp != nil { + _ = resp.Body.Close() + } + require.Error(t, err) + assert.ErrorContains(t, err, "can only call allowed HTTP servers", + "avatar client must refuse a link-local cloud-metadata address") +} diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go index 7d67b2fa3f..9492462afa 100644 --- a/routers/web/auth/openid.go +++ b/routers/web/auth/openid.go @@ -213,7 +213,7 @@ func signInOpenIDVerify(ctx *context.Context) { if u != nil { nickname = u.LowerName } - if err := updateSession(ctx, nil, map[string]any{ + if err := regenerateSession(ctx, nil, map[string]any{ "openid_verified_uri": id, "openid_determined_email": email, "openid_determined_username": nickname, diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go index bc6fdeb907..278d709c09 100644 --- a/routers/web/devtest/mock_actions.go +++ b/routers/web/devtest/mock_actions.go @@ -85,6 +85,7 @@ func MockActionsRunsJobs(ctx *context.Context) { } resp := &actions.ViewResponse{} resp.State.Run.RepoID = 12345 + resp.State.Run.Index = runID resp.State.Run.TitleHTML = `mock run title link` resp.State.Run.Link = setting.AppSubURL + "/devtest/repo-action-view/runs/" + strconv.FormatInt(runID, 10) resp.State.Run.CanDeleteArtifact = true @@ -199,17 +200,20 @@ func MockActionsRunsJobs(ctx *context.Context) { resp.State.Run.CanRerunFailed = runID == 30 && isLatestAttempt // Mock job summaries so the devtest page can preview the Summary panel rendering. - resp.State.Run.JobSummaries = []*actions.ViewJobSummary{ - { - JobID: runID * 10, - JobName: "job 100 (testsubname)", - SummaryHTML: renderUtils.MarkdownToHtml("### Devtest job summary\n\n- Markdown rendering\n- Links: [example](https://example.com)\n\n```sh\necho hello\n```\n"), - }, - { - JobID: runID*10 + 2, - JobName: "ULTRA LOOOOOOOOOOOONG job name 102 that exceeds the limit", - SummaryHTML: renderUtils.MarkdownToHtml("### Another summary\n\nThis demonstrates multiple job summaries in one run.\n\n- Item A\n- Item B\n"), - }, + // Only some runs have summaries, so the page also exercises the "no summary" state. + if runID == 10 || runID == 20 { + resp.State.Run.JobSummaries = []*actions.ViewJobSummary{ + { + JobID: runID * 10, + JobName: "job 100 (testsubname)", + SummaryHTML: renderUtils.MarkdownToHtml("### Devtest job summary\n\n- Markdown rendering\n- Links: [example](https://example.com)\n\n```sh\necho hello\n```\n"), + }, + { + JobID: runID*10 + 2, + JobName: "ULTRA LOOOOOOOOOOOONG job name 102 that exceeds the limit", + SummaryHTML: renderUtils.MarkdownToHtml("### Another summary\n\nThis demonstrates multiple job summaries in one run.\n\n- Item A\n- Item B\n"), + }, + } } resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{ @@ -378,6 +382,8 @@ func MockActionsRunsJobs(ctx *context.Context) { // └ deep_job (regular) // cross_caller (caller, cross-repo, expanded) // └ external_job (regular) + // build (linux|windows|macos) (regular matrix; graph folds into one "build" node) + // build-call (linux|windows|macos) (caller matrix, each calls build.yml; folds into one "build-call" node like "build") // final (regular, needs local_caller + cross_caller) const ( prepareID = int64(400) @@ -388,6 +394,21 @@ func MockActionsRunsJobs(ctx *context.Context) { crossCallerID = int64(405) externalJobID = int64(406) finalID = int64(407) + + // Regular matrix set – the graph already folds these into a single "build" node. + buildLinuxID = int64(410) + buildWindowsID = int64(411) + buildMacosID = int64(412) + + // Caller matrix set – each matrix leg calls the same reusable workflow. #38466: like the + // regular "build" matrix above, these fold into one "build-call" node. Matrix legs share a + // single JobID, so the legs below use JobID "build-call" and differ only by their name suffix. + buildCallLinuxID = int64(420) + buildCallWindowsID = int64(421) + buildCallMacosID = int64(422) + buildCallLinuxJobID = int64(423) + buildCallWinJobID = int64(424) + buildCallMacJobID = int64(425) ) resp.State.Run.Jobs = []*actions.ViewJob{ @@ -428,6 +449,53 @@ func MockActionsRunsJobs(ctx *context.Context) { Status: actions_model.StatusWaiting.String(), Duration: "0s", ParentJobID: crossCallerID, }, + + // Regular matrix "build" – these fold into one matrix node in the graph. The matrix legs + // share a single JobID ("build"); the " (variant)" name suffix distinguishes the legs. + { + ID: buildLinuxID, Link: jobLink(buildLinuxID), JobID: "build", Name: "build (linux)", + Status: actions_model.StatusSuccess.String(), Duration: "1m", Needs: []string{"prepare"}, + }, + { + ID: buildWindowsID, Link: jobLink(buildWindowsID), JobID: "build", Name: "build (windows)", + Status: actions_model.StatusSuccess.String(), Duration: "2m", Needs: []string{"prepare"}, + }, + { + ID: buildMacosID, Link: jobLink(buildMacosID), JobID: "build", Name: "build (macos)", + Status: actions_model.StatusSuccess.String(), Duration: "90s", Needs: []string{"prepare"}, + }, + + // Caller matrix "build-call" – each leg calls the same reusable workflow. #38466: like the + // regular "build" matrix above, these fold into one node. The matrix legs share a single + // JobID ("build-call"); the " (variant)" name suffix distinguishes the legs. + { + ID: buildCallLinuxID, Link: jobLink(buildCallLinuxID), JobID: "build-call", Name: "build-call (linux)", + Status: actions_model.StatusSuccess.String(), Duration: "1m", Needs: []string{"prepare"}, + IsReusableCaller: true, CallUses: "./.gitea/workflows/build.yml", + }, + { + ID: buildCallLinuxJobID, Link: jobLink(buildCallLinuxJobID), JobID: "bc_linux_build", Name: "build", + Status: actions_model.StatusSuccess.String(), Duration: "1m", ParentJobID: buildCallLinuxID, + }, + { + ID: buildCallWindowsID, Link: jobLink(buildCallWindowsID), JobID: "build-call", Name: "build-call (windows)", + Status: actions_model.StatusSuccess.String(), Duration: "2m", Needs: []string{"prepare"}, + IsReusableCaller: true, CallUses: "./.gitea/workflows/build.yml", + }, + { + ID: buildCallWinJobID, Link: jobLink(buildCallWinJobID), JobID: "bc_windows_build", Name: "build", + Status: actions_model.StatusSuccess.String(), Duration: "2m", ParentJobID: buildCallWindowsID, + }, + { + ID: buildCallMacosID, Link: jobLink(buildCallMacosID), JobID: "build-call", Name: "build-call (macos)", + Status: actions_model.StatusSuccess.String(), Duration: "90s", Needs: []string{"prepare"}, + IsReusableCaller: true, CallUses: "./.gitea/workflows/build.yml", + }, + { + ID: buildCallMacJobID, Link: jobLink(buildCallMacJobID), JobID: "bc_macos_build", Name: "build", + Status: actions_model.StatusSuccess.String(), Duration: "90s", ParentJobID: buildCallMacosID, + }, + { ID: finalID, Link: jobLink(finalID), JobID: "final", Name: "final", Status: actions_model.StatusBlocked.String(), Duration: "0s", diff --git a/routers/web/feed/branch.go b/routers/web/feed/branch.go index 139b7ebc43..76184d6443 100644 --- a/routers/web/feed/branch.go +++ b/routers/web/feed/branch.go @@ -21,7 +21,7 @@ func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType stri var commits []*git.Commit var err error if ctx.Repo.Commit != nil { - commits, err = ctx.Repo.Commit.CommitsByRange(0, 10, "", "", "") + commits, err = ctx.Repo.Commit.CommitsByRange(ctx, ctx.Repo.GitRepo, 0, 10, "", "", "") if err != nil { ctx.ServerError("ShowBranchFeed", err) return diff --git a/routers/web/feed/file.go b/routers/web/feed/file.go index fa590dfcf0..debb6dd4c9 100644 --- a/routers/web/feed/file.go +++ b/routers/web/feed/file.go @@ -23,7 +23,7 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string if len(fileName) == 0 { return } - commits, _, err := ctx.Repo.GitRepo.CommitsByFileAndRange( + commits, _, err := ctx.Repo.GitRepo.CommitsByFileAndRange(ctx, git.CommitsByFileAndRangeOptions{ Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName File: fileName, diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go index 0d237c9c74..9fef2a26b5 100644 --- a/routers/web/feed/profile.go +++ b/routers/web/feed/profile.go @@ -41,6 +41,11 @@ func showUserFeed(ctx *context.Context, formatType string) { includePrivate = isOrgMember } + // a public-only API token must not surface private activity, even for its own owner + if includePrivate && context.TokenIsPublicOnly(ctx) { + includePrivate = false + } + actions, _, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{ RequestedUser: ctx.ContextUser, Actor: ctx.Doer, diff --git a/routers/web/goget.go b/routers/web/goget.go index ba8a311f95..256d755beb 100644 --- a/routers/web/goget.go +++ b/routers/web/goget.go @@ -11,7 +11,11 @@ import ( "path" "strings" + auth_model "gitea.dev/models/auth" + access_model "gitea.dev/models/perm/access" repo_model "gitea.dev/models/repo" + "gitea.dev/models/unit" + user_model "gitea.dev/models/user" "gitea.dev/modules/setting" "gitea.dev/modules/util" "gitea.dev/services/context" @@ -51,10 +55,8 @@ func goGet(ctx *context.Context) { return } branchName := setting.Repository.DefaultBranch - - repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName) - if err == nil && len(repo.DefaultBranch) > 0 { - branchName = repo.DefaultBranch + if repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName); err == nil { + branchName = goGetDefaultBranch(ctx, repo) } prefix := setting.AppURL + path.Join(url.PathEscape(ownerName), url.PathEscape(repoName), "src", "branch", util.PathEscapeSegments(branchName)) @@ -91,3 +93,47 @@ func goGet(ctx *context.Context) { ctx.RespHeader().Set("Content-Type", "text/html") _, _ = ctx.Write([]byte(res)) } + +// goGetDefaultBranch returns the repository's real default branch only when the caller may genuinely +// reach it, otherwise the neutral instance default, so the meta response does not disclose the branch +// name (or the repo's existence) to callers who cannot see the repository. +func goGetDefaultBranch(ctx *context.Context, repo *repo_model.Repository) string { + def := setting.Repository.DefaultBranch + if len(repo.DefaultBranch) == 0 { + return def + } + if err := repo.LoadOwner(ctx); err != nil || repo.Owner == nil { + return def + } + // a token that was not granted repository read scope must not learn repository details, even when the + // account behind it could read the repo through the web UI + if !goGetTokenCanReadRepo(ctx) { + return def + } + // a public-only token may only reach genuinely public resources (a public repo under a public owner) + if context.TokenIsPublicOnly(ctx) && (repo.IsPrivate || !repo.Owner.Visibility.IsPublic()) { + return def + } + // the caller must be able to read the code and see the owner: a limited/private owner hides its repos + // from anonymous/non-member callers even when the repo itself is public + perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) + if err != nil || !perm.CanRead(unit.TypeCode) || !user_model.IsUserVisibleToViewer(ctx, repo.Owner, ctx.Doer) { + return def + } + return repo.DefaultBranch +} + +// goGetTokenCanReadRepo reports whether the request may learn repository details. A non-token request +// always may; a token request may only when its scope grants repository read, so a PAT that was never +// scoped for repositories cannot disclose the branch even if its owner can read the repo. +func goGetTokenCanReadRepo(ctx *context.Context) bool { + if ctx.Data["IsApiToken"] != true { + return true + } + scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) + if !ok { + return false + } + has, err := scope.HasScope(auth_model.AccessTokenScopeReadRepository) + return err == nil && has +} diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 7c52455d31..19b80fa093 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -194,7 +194,7 @@ func prepareOrgProfileReadme(ctx *context.Context, prepareResult *shared_user.Pr return false } - readmeBytes, err := readmeBlob.GetBlobContent(setting.UI.MaxDisplayFileSize) + readmeBytes, err := readmeBlob.GetBlobContent(ctx, setting.UI.MaxDisplayFileSize) if err != nil { log.Error("failed to GetBlobContent for profile %q (view as %q) readme: %v", profileRepo.FullName(), viewAs, err) return false diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 0db668259c..3c0048b29b 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -450,7 +450,7 @@ func ViewProject(ctx *context.Context) { ctx.Data["MilestoneID"] = milestoneID // Get assignees. - assigneeUsers, err := project_service.LoadIssuesAssigneesForProject(ctx, issuesMap) + assigneeUsers, err := project_service.LoadIssuesAssigneesForProject(ctx, project.ID) if err != nil { ctx.ServerError("LoadIssuesAssigneesForProject", err) return diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 2dafe88cc5..9e656350d2 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -235,9 +235,10 @@ func SettingsRenamePost(ctx *context.Context) { // SettingsChangeVisibilityPost response for change organization visibility func SettingsChangeVisibilityPost(ctx *context.Context) { - visibility, ok := structs.VisibilityModes[ctx.FormString("visibility")] + visibilityStr := ctx.FormString("visibility") + visibility, ok := structs.VisibilityModes[structs.VisibilityString(visibilityStr)] if !ok { - ctx.Flash.Error(ctx.Tr("invalid_data", visibility)) + ctx.Flash.Error(ctx.Tr("invalid_data", visibilityStr)) ctx.JSONRedirect(setting.AppSubURL + "/org/" + url.PathEscape(ctx.Org.Organization.Name) + "/settings") return } diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 9c5e1664de..04a6142ed5 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -8,6 +8,7 @@ import ( stdCtx "context" "errors" "fmt" + "html/template" "net/http" "net/url" "slices" @@ -19,6 +20,7 @@ import ( repo_model "gitea.dev/models/repo" "gitea.dev/models/unit" "gitea.dev/modules/actions" + "gitea.dev/modules/base" "gitea.dev/modules/container" "gitea.dev/modules/git" "gitea.dev/modules/log" @@ -42,9 +44,15 @@ const ( ) type WorkflowInfo struct { - Entry git.TreeEntry - ErrMsg string - Workflow *act_model.Workflow + EntryName string + ErrMsg string + Workflow *act_model.Workflow +} + +type workflowBadge struct { + DisplayName string + BadgeURL string + WorkflowURL string } // DisplayName returns the workflow name from the YAML file if present, otherwise the filename. @@ -52,7 +60,7 @@ func (w WorkflowInfo) DisplayName() string { if w.Workflow != nil && w.Workflow.Name != "" { return w.Workflow.Name } - return w.Entry.Name() + return w.EntryName } // MustEnableActions check if actions are enabled in settings @@ -79,7 +87,7 @@ func List(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("actions.actions") ctx.Data["PageIsActions"] = true - commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) + commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx, ctx.Repo.Repository.DefaultBranch) if errors.Is(err, util.ErrNotExist) { ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch) ctx.NotFound(nil) @@ -93,36 +101,62 @@ func List(ctx *context.Context) { if ctx.Written() { return } - otherWorkflows := prepareOtherWorkflows(ctx, workflows, curWorkflowID) + + data := prepareWorkflowList(ctx, curWorkflowID) + if data.partialRefresh { + // TODO: at the moment, the "partial refresh" depends on some heavy git&db operations (especially finding & parsing the workflow files) + // If it becomes a real problem in the future, maybe some "caches" can be used to optimize. + if !data.preparePartialRefreshRuns(ctx) { + return + } + if !data.prepareCommon(ctx, workflows) { + return + } + ctx.HTML(http.StatusOK, "repo/actions/runs_list") + return + } + + scopedNames := prepareScopedWorkflows(ctx, curWorkflowID, data.scopedWorkflowSourceRepoID) if ctx.Written() { return } - prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID) + otherWorkflows := prepareOtherWorkflows(ctx, workflows, scopedNames, curWorkflowID) + if ctx.Written() { + return + } + prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID, data.scopedWorkflowSourceRepoID) if ctx.Written() { return } - prepareWorkflowList(ctx, workflows, otherWorkflows) - if ctx.Written() { + if !data.prepareFullPageRuns(ctx, otherWorkflows) { + return + } + if !data.prepareCommon(ctx, workflows) { + return + } + if !data.prepareFullPageTemplate(ctx) { return } + ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(otherWorkflows) > 0 || len(data.ActionRuns) > 0 || len(scopedNames) > 0 ctx.HTML(http.StatusOK, tplListActions) } // prepareOtherWorkflows surfaces historical runs whose workflow file no longer // exists on the default branch (renamed, removed, or only on other branches). -func prepareOtherWorkflows(ctx *context.Context, workflows []WorkflowInfo, curWorkflowID string) []string { +func prepareOtherWorkflows(ctx *context.Context, workflows []WorkflowInfo, scopedNames container.Set[string], curWorkflowID string) []string { listed := make(container.Set[string], len(workflows)) for _, w := range workflows { - listed.Add(w.Entry.Name()) + listed.Add(w.EntryName) } var other []string if ctx.Repo.Repository.NumActionRuns > 0 { - ids, err := actions_model.GetRunWorkflowIDs(ctx, ctx.Repo.Repository.ID) + // "Other workflows" lists repo-level orphans only: GetRepoRunWorkflowIDs excludes scoped runs. + ids, err := actions_model.GetRepoRunWorkflowIDs(ctx, ctx.Repo.Repository.ID) if err != nil { - ctx.ServerError("GetRunWorkflowIDs", err) + ctx.ServerError("GetRepoRunWorkflowIDs", err) return nil } other = container.FilterSlice(ids, func(id string) (string, bool) { @@ -131,7 +165,8 @@ func prepareOtherWorkflows(ctx *context.Context, workflows []WorkflowInfo, curWo } ctx.Data["OtherWorkflows"] = other - ctx.Data["CurWorkflowIsListed"] = curWorkflowID == "" || listed.Contains(curWorkflowID) + // A selected workflow counts as "listed" if it is a repo-level file or an active scoped workflow. + ctx.Data["CurWorkflowIsListed"] = curWorkflowID == "" || listed.Contains(curWorkflowID) || scopedNames.Contains(curWorkflowID) return other } @@ -146,9 +181,9 @@ func WorkflowDispatchInputs(ctx *context.Context) { var commit *git.Commit var err error if refName.IsTag() { - commit, err = ctx.Repo.GitRepo.GetTagCommit(refName.TagName()) + commit, err = ctx.Repo.GitRepo.GetTagCommit(ctx, refName.TagName()) } else if refName.IsBranch() { - commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName()) + commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx, refName.BranchName()) } else { ctx.ServerError("UnsupportedRefType", nil) return @@ -161,7 +196,7 @@ func WorkflowDispatchInputs(ctx *context.Context) { if ctx.Written() { return } - prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID) + prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID, ctx.FormInt64("scoped_workflow_source_repo_id")) if ctx.Written() { return } @@ -171,7 +206,7 @@ func WorkflowDispatchInputs(ctx *context.Context) { func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflows []WorkflowInfo, curWorkflowID string) { curWorkflowID = ctx.FormString("workflow") - _, entries, err := actions.ListWorkflows(commit) + _, entries, err := actions.ListWorkflows(ctx, ctx.Repo.GitRepo, commit) if err != nil { ctx.ServerError("ListWorkflows", err) return nil, "" @@ -179,8 +214,8 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow workflows = make([]WorkflowInfo, 0, len(entries)) for _, entry := range entries { - workflow := WorkflowInfo{Entry: *entry} - content, err := actions.GetContentFromEntry(entry) + workflow := WorkflowInfo{EntryName: entry.Name()} + content, err := actions.GetContentFromEntry(ctx, ctx.Repo.GitRepo, entry) if err != nil { ctx.ServerError("GetContentFromEntry", err) return nil, "" @@ -229,6 +264,7 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow ctx.Data["workflows"] = workflows ctx.Data["RepoLink"] = ctx.Repo.Repository.Link() + ctx.Data["RepoID"] = ctx.Repo.Repository.ID ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.Permission.IsAdmin() actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() ctx.Data["ActionsConfig"] = actionsConfig @@ -238,21 +274,165 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow return workflows, curWorkflowID } -func prepareWorkflowDispatchTemplate(ctx *context.Context, workflowInfos []WorkflowInfo, curWorkflowID string) { - actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() - if curWorkflowID == "" || !ctx.Repo.Permission.CanWrite(unit.TypeActions) || actionsConfig.IsWorkflowDisabled(curWorkflowID) { +// ScopedWorkflowInfo describes a scoped workflow effective for the current repo, listed under its source group. +type ScopedWorkflowInfo struct { + SourceRepoID int64 + EntryName string + DisplayName string + Required bool + Disabled bool +} + +// ScopedWorkflowSourceGroup groups the scoped workflows contributed by one source repo for the All-Workflows sidebar. +type ScopedWorkflowSourceGroup struct { + SourceRepoID int64 + SourceRepoName string // owner/name of the source repo; shown for instance-level sources and used as the tooltip + SourceRepoShortName string // name only; shown for owner-level sources, where the owner is always the current owner + FromInstance bool // registered at instance level (owner_id == 0) rather than by the owner + IsActive bool // the currently-selected workflow belongs to this source; render the group expanded + Workflows []ScopedWorkflowInfo +} + +// prepareScopedWorkflows lists the scoped workflows effective for the repo's owner (and instance) for the All-Workflows sidebar. +func prepareScopedWorkflows(ctx *context.Context, curWorkflowID string, curWorkflowRepoID int64) container.Set[string] { + scopedNames := make(container.Set[string]) + + repo := ctx.Repo.Repository + sources, err := actions_model.GetEffectiveScopedWorkflowSources(ctx, repo.OwnerID) + if err != nil { + ctx.ServerError("GetEffectiveScopedWorkflowSources", err) + return scopedNames + } + if len(sources) == 0 { + return scopedNames + } + + actionsConfig := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() + + groups := make([]ScopedWorkflowSourceGroup, 0, len(sources)) + seen := make(map[int64]bool, len(sources)) + for _, source := range sources { + if seen[source.SourceRepoID] { + continue + } + seen[source.SourceRepoID] = true + + sourceRepo, err := repo_model.GetRepositoryByID(ctx, source.SourceRepoID) + if err != nil { + log.Error("scoped workflows list: load source repo %d: %v", source.SourceRepoID, err) + continue + } + if sourceRepo.IsEmpty { + continue + } + + _, entries, err := actions_service.LoadParsedScopedWorkflows(ctx, sourceRepo) + if err != nil { + log.Error("scoped workflows list: parse %s: %v", sourceRepo.FullName(), err) + continue + } + if len(entries) == 0 { + continue + } + + group := ScopedWorkflowSourceGroup{ + SourceRepoID: sourceRepo.ID, + SourceRepoName: sourceRepo.FullName(), + SourceRepoShortName: sourceRepo.Name, + FromInstance: source.OwnerID == 0, + } + for _, e := range entries { + scopedNames.Add(e.EntryName) + required := actions_model.IsWorkflowRequiredInSources(sources, sourceRepo.ID, e.EntryName) + disabled := actionsConfig.IsScopedWorkflowDisabled(sourceRepo.ID, e.EntryName) + group.Workflows = append(group.Workflows, ScopedWorkflowInfo{ + SourceRepoID: sourceRepo.ID, + EntryName: e.EntryName, + DisplayName: e.DisplayName, + Required: required, + Disabled: disabled, + }) + + if curWorkflowID == e.EntryName && curWorkflowRepoID == sourceRepo.ID { + ctx.Data["CurWorkflowDisabled"] = disabled + ctx.Data["CurWorkflowScopedRepoID"] = sourceRepo.ID + ctx.Data["CurWorkflowRequired"] = required + group.IsActive = true // keep this group expanded so the selected workflow stays visible + } + } + groups = append(groups, group) + } + + ctx.Data["ScopedWorkflowGroups"] = groups + return scopedNames +} + +// loadScopedWorkflowModel reads and parses a scoped workflow's content from its source repo's default branch. +func loadScopedWorkflowModel(ctx *context.Context, repo *repo_model.Repository, sourceRepoID int64, workflowID string) *act_model.Workflow { + effective, err := actions_model.IsScopedWorkflowSourceEffective(ctx, repo.OwnerID, sourceRepoID) + if err != nil { + log.Error("scoped dispatch: IsScopedWorkflowSourceEffective: %v", err) + return nil + } + if !effective { + return nil + } + + sourceRepo, err := repo_model.GetRepositoryByID(ctx, sourceRepoID) + if err != nil || sourceRepo.IsEmpty { + return nil + } + content, err := actions_service.ScopedWorkflowContent(ctx, sourceRepo, workflowID) + if err != nil { + log.Error("scoped dispatch: content of %s in %s: %v", workflowID, sourceRepo.FullName(), err) + return nil + } + if content == nil { + return nil // the workflow does not exist on the source's default branch + } + wf, err := act_model.ReadWorkflow(bytes.NewReader(content)) + if err != nil { + return nil + } + return wf +} + +func prepareWorkflowDispatchTemplate(ctx *context.Context, workflowInfos []WorkflowInfo, curWorkflowID string, curWorkflowRepoID int64) { + repo := ctx.Repo.Repository + if curWorkflowID == "" || !ctx.Repo.Permission.CanWrite(unit.TypeActions) { + return + } + actionsConfig := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() + + isScoped := curWorkflowRepoID > 0 + if isScoped { + // a required scoped workflow can never be opted out, so a stale disabled flag must not hide its dispatch form + optedOut, err := actions_model.IsScopedWorkflowOptedOut(ctx, actionsConfig, repo.OwnerID, curWorkflowRepoID, curWorkflowID) + if err != nil { + log.Error("IsScopedWorkflowOptedOut: %v", err) + return + } + if optedOut { + return + } + } else if actionsConfig.IsWorkflowDisabled(curWorkflowID) { return } var curWorkflow *act_model.Workflow - for _, workflowInfo := range workflowInfos { - if workflowInfo.Entry.Name() == curWorkflowID { - if workflowInfo.Workflow == nil { - log.Debug("CurWorkflowID %s is found but its workflowInfo.Workflow is nil", curWorkflowID) - return + if isScoped { + // a scoped workflow's content lives in its source repo, not in workflowInfos (the consumer's own files) + curWorkflow = loadScopedWorkflowModel(ctx, repo, curWorkflowRepoID, curWorkflowID) + } else { + for _, workflowInfo := range workflowInfos { + if workflowInfo.EntryName == curWorkflowID { + if workflowInfo.Workflow == nil { + log.Debug("CurWorkflowID %s is found but its workflowInfo.Workflow is nil", curWorkflowID) + return + } + curWorkflow = workflowInfo.Workflow + break } - curWorkflow = workflowInfo.Workflow - break } } @@ -293,64 +473,66 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, workflowInfos []Workf ctx.Data["Tags"] = tags } -func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo, otherWorkflows []string) { - actorID := ctx.FormInt64("actor") - status := ctx.FormInt("status") - workflowID := ctx.FormString("workflow") - branch := ctx.FormString("branch") - page := ctx.FormInt("page") - if page <= 0 { - page = 1 - } - - // if status or actor query param is not given to frontend href, (href="//actions") - // they will be 0 by default, which indicates get all status or actors - ctx.Data["CurActor"] = actorID - ctx.Data["CurStatus"] = status - ctx.Data["CurBranch"] = branch - if actorID > 0 || status > int(actions_model.StatusUnknown) || branch != "" { - ctx.Data["IsFiltered"] = true - } - +func (data *actionRunListData) prepareFullPageRuns(ctx *context.Context, otherWorkflows []string) bool { opts := actions_model.FindRunOptions{ ListOptions: db.ListOptions{ - Page: page, + Page: max(ctx.FormInt("page"), 1), PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), }, - RepoID: ctx.Repo.Repository.ID, - WorkflowID: workflowID, - TriggerUserID: actorID, + RepoID: ctx.Repo.Repository.ID, + WorkflowID: data.workflowID, + WorkflowRepoID: data.scopedWorkflowSourceRepoID, + TriggerUserID: data.actorID, + } + + // Constrain scoped vs repo-level only for a listed workflow, whose link carries scoped_workflow_source_repo_id. + if data.workflowID != "" && !slices.Contains(otherWorkflows, data.workflowID) { + opts.IsScopedRun = optional.Some(data.scopedWorkflowSourceRepoID > 0) } // if status is not StatusUnknown, it means user has selected a status filter - if actions_model.Status(status) != actions_model.StatusUnknown { - opts.Status = []actions_model.Status{actions_model.Status(status)} + if data.status != actions_model.StatusUnknown { + opts.Status = []actions_model.Status{data.status} } - if branch != "" { - opts.Ref = string(git.RefNameFromBranch(branch)) + if data.branch != "" { + opts.Ref = string(git.RefNameFromBranch(data.branch)) } runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts) if err != nil { ctx.ServerError("FindAndCount", err) - return + return false } + data.pager = context.NewPagination(total, opts.PageSize, opts.Page, 5) + data.pager.AddParamFromRequest(ctx.Req) + data.ActionRuns = runs + return true +} - for _, run := range runs { - run.Repo = ctx.Repo.Repository - } +type actionRunListData struct { + actorID int64 + status actions_model.Status + workflowID string + workflowDisplayName string + scopedWorkflowSourceRepoID int64 + branch string + refreshRunIDs []int64 + partialRefresh bool + pager *context.Pagination - if err := actions_model.RunList(runs).LoadTriggerUser(ctx); err != nil { - ctx.ServerError("LoadTriggerUser", err) - return - } - - if err := loadIsRefDeleted(ctx, ctx.Repo.Repository.ID, runs); err != nil { - log.Error("LoadIsRefDeleted", err) - } + // the following fields are used by the template + RefreshLink template.URL + RefreshIntervalMs int64 + ActionRuns []*actions_model.ActionRun + IsFiltered bool + WorkflowNames map[string]string + RunErrors map[int64]string + CurWorkflow string + CanWriteRepoUnitActions bool +} +func (data *actionRunListData) processActionRuns(ctx *context.Context) bool { // Check for each run if there is at least one online runner that can run its jobs - runErrors := make(map[int64]string) runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{ RepoID: ctx.Repo.Repository.ID, IsOnline: optional.Some(true), @@ -358,28 +540,30 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo, otherWo }) if err != nil { ctx.ServerError("FindRunners", err) - return + return false } - for _, run := range runs { + + data.RunErrors = make(map[int64]string) + for _, run := range data.ActionRuns { if !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning, actions_model.StatusBlocked) { continue } jobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(ctx, run.RepoID, run.ID) if err != nil { ctx.ServerError("GetRunJobsByRunID", err) - return + return false } for _, job := range jobs { if !job.Status.In(actions_model.StatusWaiting, actions_model.StatusBlocked) { continue } if err := actions.ValidateWorkflowContent(job.WorkflowPayload); err != nil { - runErrors[run.ID] = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) + data.RunErrors[run.ID] = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) break } if job.CallUses != "" { if _, err := actions_service.ResolveUses(ctx, job.CallUses); err != nil { - runErrors[run.ID] = ctx.Locale.TrString("actions.runs.invalid_reusable_workflow_uses", err.Error()) + data.RunErrors[run.ID] = ctx.Locale.TrString("actions.runs.invalid_reusable_workflow_uses", err.Error()) break } } @@ -392,44 +576,149 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo, otherWo } } if !hasOnlineRunner { - runErrors[run.ID] = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", strings.Join(job.RunsOn, ",")) + data.RunErrors[run.ID] = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", strings.Join(job.RunsOn, ",")) break } } } } - ctx.Data["RunErrors"] = runErrors + return true +} - ctx.Data["Runs"] = runs - - workflowNames := make(map[string]string, len(workflows)) - for _, wf := range workflows { - workflowNames[wf.Entry.Name()] = wf.DisplayName() +func (data *actionRunListData) fillRefreshMeta(ctx *context.Context) bool { + hasActiveRuns := false + var actionRunIDs []int64 + for _, run := range data.ActionRuns { + if !hasActiveRuns && run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning, actions_model.StatusBlocked) { + hasActiveRuns = true + } + actionRunIDs = append(actionRunIDs, run.ID) } - ctx.Data["WorkflowNames"] = workflowNames + data.RefreshIntervalMs = util.Iif[int64](hasActiveRuns, 3*1000, 12*1000) + if !setting.IsProd { + data.RefreshIntervalMs = util.Iif[int64](hasActiveRuns, 1000, 2*1000) // faster in dev mode to make debug easier + } + if len(data.ActionRuns) == 0 { + data.RefreshIntervalMs = 0 + } + // Only refresh the items on the current page, do not keep loading "new" items. + // Otherwise, when end users are browsing the list page, they will just keep seeing "new" items coming in + // and "old" items disappearing, they won't be able to focus on an item that they are interested in, which is a bad user experience. + data.RefreshLink = templates.QueryBuild(setting.AppSubURL+ctx.Req.RequestURI, "run_ids", strings.Join(base.Int64sToStrings(actionRunIDs), ",")) + return true +} +func (data *actionRunListData) fillWorkflowNames(workflows []WorkflowInfo) bool { + data.WorkflowNames = make(map[string]string, len(workflows)) + data.workflowDisplayName = data.workflowID + for _, wf := range workflows { + displayName := wf.DisplayName() + data.WorkflowNames[wf.EntryName] = displayName + if wf.EntryName == data.workflowID { + data.workflowDisplayName = displayName + } + } + return true +} + +func prepareWorkflowList(ctx *context.Context, curWorkflowID string) *actionRunListData { + data := &actionRunListData{} + data.actorID = ctx.FormInt64("actor") + data.status = actions_model.Status(ctx.FormInt("status")) + data.workflowID = ctx.FormString("workflow") + data.scopedWorkflowSourceRepoID = ctx.FormInt64("scoped_workflow_source_repo_id") + data.branch = ctx.FormString("branch") + data.refreshRunIDs = ctx.FormStringInt64s("run_ids") + data.partialRefresh = ctx.Req.Form.Has("run_ids") + + data.IsFiltered = data.actorID > 0 || data.status != actions_model.StatusUnknown || data.branch != "" + data.CurWorkflow = curWorkflowID + data.CanWriteRepoUnitActions = ctx.Repo.Permission.CanWrite(unit.TypeActions) + return data +} + +func (data *actionRunListData) preparePartialRefreshRuns(ctx *context.Context) bool { + runs, err := actions_model.GetRunsByRepoAndID(ctx, ctx.Repo.Repository.ID, data.refreshRunIDs) + if err != nil { + ctx.ServerError("GetRunsByRepoAndID", err) + return false + } + data.ActionRuns = runs + return true +} + +func (data *actionRunListData) prepareCommon(ctx *context.Context, workflows []WorkflowInfo) bool { + for _, run := range data.ActionRuns { + run.Repo = ctx.Repo.Repository + } + + if err := actions_model.RunList(data.ActionRuns).LoadTriggerUser(ctx); err != nil { + ctx.ServerError("LoadTriggerUser", err) + return false + } + if err := loadIsRefDeleted(ctx, ctx.Repo.Repository.ID, data.ActionRuns); err != nil { + log.Error("LoadIsRefDeleted", err) + } + + if !data.processActionRuns(ctx) { + return false + } + if !data.fillRefreshMeta(ctx) { + return false + } + if !data.fillWorkflowNames(workflows) { + return false + } + ctx.Data["ActionRunListData"] = data + return true +} + +func (data *actionRunListData) prepareFullPageTemplate(ctx *context.Context) bool { + // A scoped workflow has no repo-level badge on this repo (the badge endpoint reads is_scoped_run=false runs), + // so don't offer the "create status badge" entry for it. + if data.scopedWorkflowSourceRepoID == 0 { + prepareWorkflowBadgeTemplate(ctx, data.workflowID, data.workflowDisplayName) + } + + // fill template data actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID) if err != nil { ctx.ServerError("GetActors", err) - return + return false } ctx.Data["Actors"] = shared_user.MakeSelfOnTop(ctx.Doer, actors) - ctx.Data["StatusInfoList"] = actions_model.GetStatusInfoList(ctx, ctx.Locale) - runBranches, err := actions_model.GetRunBranches(ctx, ctx.Repo.Repository.ID) if err != nil { ctx.ServerError("GetRunBranches", err) - return + return false } ctx.Data["RunBranches"] = runBranches - pager := context.NewPagination(total, opts.PageSize, opts.Page, 5) - pager.AddParamFromRequest(ctx.Req) - ctx.Data["Page"] = pager - ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(otherWorkflows) > 0 || len(runs) > 0 + ctx.Data["StatusInfoList"] = actions_model.GetStatusInfoList(ctx, ctx.Locale) + ctx.Data["CurActor"] = data.actorID + ctx.Data["CurStatus"] = int(data.status) // don't make template use its "String" method, otherwise the query parameter would be wrong + ctx.Data["CurBranch"] = data.branch + ctx.Data["CurWorkflowRepoID"] = data.scopedWorkflowSourceRepoID + ctx.Data["Page"] = data.pager + return true +} - ctx.Data["CanWriteRepoUnitActions"] = ctx.Repo.Permission.CanWrite(unit.TypeActions) +func prepareWorkflowBadgeTemplate(ctx *context.Context, workflowID, displayName string) { + if workflowID == "" { + return + } + displayName = util.IfZero(displayName, workflowID) + + repoURL := ctx.Repo.Repository.HTMLURL(ctx) + badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", repoURL, util.PathEscapeSegments(workflowID), url.QueryEscape(ctx.Repo.Repository.DefaultBranch)) + workflowURL := fmt.Sprintf("%s/actions?workflow=%s", repoURL, url.QueryEscape(workflowID)) + + ctx.Data["WorkflowBadge"] = workflowBadge{ + BadgeURL: badgeURL, + WorkflowURL: workflowURL, + DisplayName: displayName, + } } // loadIsRefDeleted loads the IsRefDeleted field for each run in the list. @@ -541,10 +830,11 @@ func decodeNode(node yaml.Node, out any) bool { return true } -func actionsListRedirectURL(repoLink, workflow, actor, status, branch string) string { - return fmt.Sprintf("%s/actions?workflow=%s&actor=%s&status=%s&branch=%s", +func actionsListRedirectURL(repoLink, workflow, scopedWorkflowSourceRepoID, actor, status, branch string) string { + return fmt.Sprintf("%s/actions?workflow=%s&scoped_workflow_source_repo_id=%s&actor=%s&status=%s&branch=%s", repoLink, url.QueryEscape(workflow), + url.QueryEscape(scopedWorkflowSourceRepoID), url.QueryEscape(actor), url.QueryEscape(status), url.QueryEscape(branch), diff --git a/routers/web/repo/actions/actions_test.go b/routers/web/repo/actions/actions_test.go index 1f1040738b..7775362168 100644 --- a/routers/web/repo/actions/actions_test.go +++ b/routers/web/repo/actions/actions_test.go @@ -4,12 +4,18 @@ package actions import ( + "net/http" + "net/http/httptest" "strings" "testing" actions_model "gitea.dev/models/actions" "gitea.dev/models/db" + repo_model "gitea.dev/models/repo" unittest "gitea.dev/models/unittest" + "gitea.dev/modules/setting" + "gitea.dev/modules/test" + web_context "gitea.dev/services/context" act_model "gitea.com/gitea/runner/act/model" "github.com/stretchr/testify/assert" @@ -176,3 +182,41 @@ func Test_loadIsRefDeleted(t *testing.T) { assert.True(t, run.IsRefDeleted) } } + +func TestPrepareWorkflowBadgeTemplate(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://gitea.example.com/")() + + t.Run("no workflow selected", func(t *testing.T) { + ctx := newWorkflowBadgeTestContext(t) + + prepareWorkflowBadgeTemplate(ctx, "", "ignored") + + assert.NotContains(t, ctx.Data, "WorkflowBadge") + }) + + t.Run("selected workflow", func(t *testing.T) { + ctx := newWorkflowBadgeTestContext(t) + + prepareWorkflowBadgeTemplate(ctx, "build/test workflow.yml", `CI [prod]\build "fast" `) + + assert.Equal(t, workflowBadge{ + BadgeURL: "https://gitea.example.com/user1/repo1/actions/workflows/build/test%20workflow.yml/badge.svg?branch=release%2F1.0+%26+hotfix", + WorkflowURL: "https://gitea.example.com/user1/repo1/actions?workflow=build%2Ftest+workflow.yml", + DisplayName: `CI [prod]\build "fast" `, + }, ctx.Data["WorkflowBadge"]) + }) +} + +func newWorkflowBadgeTestContext(t *testing.T) *web_context.Context { + t.Helper() + + req := httptest.NewRequest(http.MethodGet, "https://gitea.example.com/user1/repo1/actions", nil) + resp := httptest.NewRecorder() + ctx := web_context.NewWebContext(web_context.NewBaseContextForTest(resp, req), nil, nil) + ctx.Repo.Repository = &repo_model.Repository{ + OwnerName: "user1", + Name: "repo1", + DefaultBranch: "release/1.0 & hotfix", + } + return ctx +} diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 2f4f1950ec..1ffb180d52 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -21,15 +21,18 @@ import ( "gitea.dev/models/db" git_model "gitea.dev/models/git" issues_model "gitea.dev/models/issues" + access_model "gitea.dev/models/perm/access" repo_model "gitea.dev/models/repo" "gitea.dev/models/unit" "gitea.dev/modules/actions" "gitea.dev/modules/base" "gitea.dev/modules/cache" "gitea.dev/modules/git" + "gitea.dev/modules/gitrepo" "gitea.dev/modules/httplib" "gitea.dev/modules/json" "gitea.dev/modules/log" + "gitea.dev/modules/optional" "gitea.dev/modules/storage" api "gitea.dev/modules/structs" "gitea.dev/modules/templates" @@ -243,14 +246,19 @@ func ViewWorkflowFile(ctx *context_module.Context) { return } - commit, err := ctx.Repo.GitRepo.GetCommit(run.CommitSHA) + if run.IsScopedRun { + viewScopedWorkflowFile(ctx, run) + return + } + + commit, err := ctx.Repo.GitRepo.GetCommit(ctx, run.CommitSHA) if err != nil { ctx.NotFoundOrServerError("GetCommit", func(err error) bool { return errors.Is(err, util.ErrNotExist) }, err) return } - rpath, entries, err := actions.ListWorkflows(commit) + rpath, entries, err := actions.ListWorkflows(ctx, ctx.Repo.GitRepo, commit) if err != nil { ctx.ServerError("ListWorkflows", err) return @@ -293,24 +301,26 @@ type ViewResponse struct { // ViewLink is the attempt-aware URL for navigation, e.g. "/owner/repo/actions/runs/123" for the latest attempt // or "/owner/repo/actions/runs/123/attempts/2" for a historical attempt. // Use this when the target should reflect the currently-viewed attempt. - ViewLink string `json:"viewLink"` - Title string `json:"title"` - TitleHTML template.HTML `json:"titleHTML"` - Status string `json:"status"` - CanCancel bool `json:"canCancel"` - CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve - CanRerun bool `json:"canRerun"` - CanRerunFailed bool `json:"canRerunFailed"` - CanDeleteArtifact bool `json:"canDeleteArtifact"` - Done bool `json:"done"` - WorkflowID string `json:"workflowID"` - WorkflowLink string `json:"workflowLink"` - IsSchedule bool `json:"isSchedule"` - RunAttempt int64 `json:"runAttempt"` - Attempts []*ViewRunAttempt `json:"attempts"` - Jobs []*ViewJob `json:"jobs"` - Commit ViewCommit `json:"commit"` - PullRequest *ViewPullRequest `json:"pullRequest,omitempty"` + ViewLink string `json:"viewLink"` + Index int64 `json:"index"` // the per-repository run number, displayed as "#N" + Title string `json:"title"` + TitleHTML template.HTML `json:"titleHTML"` + Status string `json:"status"` + CanCancel bool `json:"canCancel"` + CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve + CanRerun bool `json:"canRerun"` + CanRerunFailed bool `json:"canRerunFailed"` + CanDeleteArtifact bool `json:"canDeleteArtifact"` + Done bool `json:"done"` + WorkflowID string `json:"workflowID"` + WorkflowLink string `json:"workflowLink"` + CanViewWorkflowFile bool `json:"canViewWorkflowFile"` + IsSchedule bool `json:"isSchedule"` + RunAttempt int64 `json:"runAttempt"` + Attempts []*ViewRunAttempt `json:"attempts"` + Jobs []*ViewJob `json:"jobs"` + Commit ViewCommit `json:"commit"` + PullRequest *ViewPullRequest `json:"pullRequest,omitempty"` // Summary view: run duration and trigger time/event Duration string `json:"duration"` TriggeredAt int64 `json:"triggeredAt"` // unix seconds for relative time @@ -561,6 +571,7 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse, isLatestAttempt := run.LatestAttemptID == 0 || (attempt != nil && attempt.ID == run.LatestAttemptID) resp.State.Run.RepoID = ctx.Repo.Repository.ID + resp.State.Run.Index = run.Index // the title for the "run" is from the commit message resp.State.Run.Title = run.Title resp.State.Run.TitleHTML = templates.NewRenderUtils(ctx).RenderCommitMessage(run.Title, ctx.Repo.Repository) @@ -598,6 +609,11 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse, if isLatestAttempt { resp.State.Run.WorkflowLink = run.WorkflowLink() } + resp.State.Run.CanViewWorkflowFile = true + if run.IsScopedRun { + // For a scoped run the workflow file lives in the source repo; only show its link when the viewer can read that repo. + resp.State.Run.CanViewWorkflowFile = canViewScopedWorkflowFile(ctx, run) + } resp.State.Run.IsSchedule = run.IsSchedule() resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json for _, v := range jobs { @@ -701,9 +717,10 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse, resp.Artifacts = make([]*ArtifactsViewItem, 0, len(arts)) for _, art := range arts { resp.Artifacts = append(resp.Artifacts, &ArtifactsViewItem{ - Name: art.ArtifactName, - Size: art.FileSize, - Status: util.Iif(art.Status == actions_model.ArtifactStatusExpired, "expired", "completed"), + Name: art.ArtifactName, + Size: art.FileSize, + Status: util.Iif(art.Status == actions_model.ArtifactStatusExpired, "expired", "completed"), + ExpiresUnix: int64(art.ExpiredUnix), }) } } @@ -737,6 +754,8 @@ func fillViewRunResponseCurrentJob(ctx *context_module.Context, resp *ViewRespon resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale) if run.NeedApproval { resp.State.CurrentJob.Detail = ctx.Locale.TrString("actions.need_approval_desc") + } else if detail := describePendingJobDetail(ctx, current, jobs); detail != "" { + resp.State.CurrentJob.Detail = detail } resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json @@ -751,6 +770,78 @@ func fillViewRunResponseCurrentJob(ctx *context_module.Context, resp *ViewRespon } } +// describePendingJobDetail explains why a blocked or waiting job has not started +// yet, so the user can tell whether it is waiting on its dependencies or on an +// available runner. It returns an empty string when the job is not pending or the +// cause can't be determined (the caller keeps the generic status label then). +func describePendingJobDetail(ctx *context_module.Context, current *actions_model.ActionRunJob, jobs []*actions_model.ActionRunJob) string { + switch { + case current.Status.IsBlocked(): + // A blocked job is held back by the jobs listed in its `needs`. + if pending := pendingNeeds(current, jobs); len(pending) > 0 { + return ctx.Locale.TrString("actions.runs.waiting_for_dependent_jobs", strings.Join(pending, ", ")) + } + case current.Status.IsWaiting(): + // A waiting job has no runner to pick it up yet. A busy runner is still + // "online", so distinguish three cases: no runner online at all, online + // runners but none match the labels, and a matching runner that is busy. + runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{ + RepoID: current.RepoID, + IsOnline: optional.Some(true), + WithAvailable: true, + }) + if err != nil { + log.Error("FindRunners for job %d: %v", current.ID, err) + return "" + } + hasOnlineRunner, hasMatchingRunner := false, false + for _, runner := range runners { + if runner.IsDisabled { + continue + } + hasOnlineRunner = true + if runner.CanMatchLabels(current.RunsOn) { + hasMatchingRunner = true + break + } + } + switch { + case !hasOnlineRunner: + return ctx.Locale.TrString("actions.runs.no_runner_online") + case !hasMatchingRunner: + return ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", strings.Join(current.RunsOn, ", ")) + default: + // A matching runner exists but hasn't claimed the job, so it is busy. + return ctx.Locale.TrString("actions.runs.waiting_for_available_runner") + } + } + return "" +} + +// pendingNeeds returns the `needs` keys of jobs the given job depends on that +// have not finished yet, scoped to the same parent job (matrix expansions of a +// need are all required to be done). Unresolved needs are treated as pending. +func pendingNeeds(current *actions_model.ActionRunJob, jobs []*actions_model.ActionRunJob) []string { + var pending []string + for _, need := range current.Needs { + found, allDone := false, true + for _, job := range jobs { + if job.ParentJobID != current.ParentJobID || job.JobID != need { + continue + } + found = true + if !job.Status.IsDone() { + allDone = false + break + } + } + if !found || !allDone { + pending = append(pending, need) + } + } + return pending +} + func convertToViewModel(ctx context.Context, locale translation.Locale, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) { var viewJobs []*ViewJobStep var logs []*ViewStepLog @@ -846,7 +937,16 @@ func checkRunRerunAllowed(ctx *context_module.Context, run *actions_model.Action } cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) cfg := cfgUnit.ActionsConfig() - if cfg.IsWorkflowDisabled(run.WorkflowID) { + disabled := cfg.IsWorkflowDisabled(run.WorkflowID) + if run.IsScopedRun { + optedOut, err := actions_model.IsScopedWorkflowOptedOut(ctx, cfg, ctx.Repo.Repository.OwnerID, run.WorkflowRepoID, run.WorkflowID) + if err != nil { + ctx.ServerError("IsScopedWorkflowOptedOut", err) + return false + } + disabled = optedOut + } + if disabled { ctx.JSONError(ctx.Locale.Tr("actions.workflow.disabled")) return false } @@ -1274,7 +1374,24 @@ func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) { cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) cfg := cfgUnit.ActionsConfig() - if isEnable { + scopedRepoID := ctx.FormInt64("scoped_workflow_source_repo_id") + if scopedRepoID > 0 { + if !isEnable { + // a required scoped workflow can never be opted out + required, err := actions_model.IsScopedWorkflowRequired(ctx, ctx.Repo.Repository.OwnerID, scopedRepoID, workflow) + if err != nil { + ctx.ServerError("IsScopedWorkflowRequired", err) + return + } + if required { + ctx.JSONError(ctx.Locale.Tr("actions.workflow.scoped_required_cannot_disable")) + return + } + cfg.DisableScopedWorkflow(scopedRepoID, workflow) + } else { + cfg.EnableScopedWorkflow(scopedRepoID, workflow) + } + } else if isEnable { cfg.EnableWorkflow(workflow) } else { cfg.DisableWorkflow(workflow) @@ -1291,13 +1408,13 @@ func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) { ctx.Flash.Success(ctx.Tr("actions.workflow.disable_success", workflow)) } - redirectURL := actionsListRedirectURL(ctx.Repo.RepoLink, workflow, + redirectURL := actionsListRedirectURL(ctx.Repo.RepoLink, workflow, ctx.FormString("scoped_workflow_source_repo_id"), ctx.FormString("actor"), ctx.FormString("status"), ctx.FormString("branch")) ctx.JSONRedirect(redirectURL) } func Run(ctx *context_module.Context) { - redirectURL := actionsListRedirectURL(ctx.Repo.RepoLink, ctx.FormString("workflow"), + redirectURL := actionsListRedirectURL(ctx.Repo.RepoLink, ctx.FormString("workflow"), ctx.FormString("scoped_workflow_source_repo_id"), ctx.FormString("actor"), ctx.FormString("status"), ctx.FormString("branch")) workflowID := ctx.FormString("workflow") @@ -1311,7 +1428,8 @@ func Run(ctx *context_module.Context) { ctx.ServerError("ref", nil) return } - _, err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error { + sourceRepoID := ctx.FormInt64("scoped_workflow_source_repo_id") + _, err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, ref, sourceRepoID, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error { for name, config := range workflowDispatch.Inputs { value := ctx.Req.PostFormValue(name) if config.Type == "boolean" { @@ -1337,3 +1455,65 @@ func Run(ctx *context_module.Context) { ctx.Flash.Success(ctx.Tr("actions.workflow.run_success", workflowID)) ctx.Redirect(redirectURL) } + +// viewScopedWorkflowFile redirects to the scoped workflow file in its SOURCE repo. +func viewScopedWorkflowFile(ctx *context_module.Context, run *actions_model.ActionRun) { + sourceRepo, err := repo_model.GetRepositoryByID(ctx, run.WorkflowRepoID) + if err != nil { + ctx.NotFoundOrServerError("GetRepositoryByID", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) + return + } + + perm, err := access_model.GetDoerRepoPermission(ctx, sourceRepo, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } + if !perm.CanRead(unit.TypeCode) { + ctx.NotFound(nil) + return + } + + sourceGitRepo, err := gitrepo.OpenRepository(sourceRepo) + if err != nil { + ctx.ServerError("OpenRepository", err) + return + } + defer sourceGitRepo.Close() + + commit, err := sourceGitRepo.GetCommit(ctx, run.WorkflowCommitSHA) + if err != nil { + ctx.NotFoundOrServerError("GetCommit", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) + return + } + rpath, entries, err := actions.ListScopedWorkflows(ctx, sourceGitRepo, commit) + if err != nil { + ctx.ServerError("ListScopedWorkflows", err) + return + } + for _, entry := range entries { + if entry.Name() == run.WorkflowID { + ctx.Redirect(fmt.Sprintf("%s/src/commit/%s/%s/%s", sourceRepo.Link(), url.PathEscape(run.WorkflowCommitSHA), util.PathEscapeSegments(rpath), util.PathEscapeSegments(run.WorkflowID))) + return + } + } + ctx.NotFound(nil) +} + +// canViewScopedWorkflowFile reports whether the viewer may follow the "Workflow file" link of a scoped run. +func canViewScopedWorkflowFile(ctx *context_module.Context, run *actions_model.ActionRun) bool { + sourceRepo, err := repo_model.GetRepositoryByID(ctx, run.WorkflowRepoID) + if err != nil { + return false + } + perm, err := access_model.GetDoerRepoPermission(ctx, sourceRepo, ctx.Doer) + if err != nil { + log.Error("GetUserRepoPermission: %v", err) + return false + } + return perm.CanRead(unit.TypeCode) +} diff --git a/routers/web/repo/actions/view_test.go b/routers/web/repo/actions/view_test.go index 020930eb38..ac93c8f612 100644 --- a/routers/web/repo/actions/view_test.go +++ b/routers/web/repo/actions/view_test.go @@ -138,3 +138,48 @@ func TestConvertToViewModelCancellingTaskDoesNotRenderRunningSteps(t *testing.T) } assert.Equal(t, expectedViewJobs, viewJobSteps) } + +func TestPendingNeeds(t *testing.T) { + current := &actions_model.ActionRunJob{JobID: "deploy", Needs: []string{"build", "test"}} + jobs := []*actions_model.ActionRunJob{ + current, + {JobID: "build", Status: actions_model.StatusSuccess}, + {JobID: "test", Status: actions_model.StatusRunning}, + } + // "test" is not done yet, "build" succeeded, so only "test" blocks. + assert.Equal(t, []string{"test"}, pendingNeeds(current, jobs)) + + t.Run("all needs done", func(t *testing.T) { + done := []*actions_model.ActionRunJob{ + current, + {JobID: "build", Status: actions_model.StatusSuccess}, + {JobID: "test", Status: actions_model.StatusSkipped}, + } + assert.Empty(t, pendingNeeds(current, done)) + }) + + t.Run("matrix expansion all required", func(t *testing.T) { + matrix := []*actions_model.ActionRunJob{ + current, + {JobID: "build", Status: actions_model.StatusSuccess}, + {JobID: "build", Status: actions_model.StatusRunning}, + {JobID: "test", Status: actions_model.StatusSuccess}, + } + assert.Equal(t, []string{"build"}, pendingNeeds(current, matrix)) + }) + + t.Run("unresolved need treated as pending", func(t *testing.T) { + missing := []*actions_model.ActionRunJob{current} + assert.Equal(t, []string{"build", "test"}, pendingNeeds(current, missing)) + }) + + t.Run("parent job scope", func(t *testing.T) { + // a same-named job under a different parent must not satisfy the need + scoped := &actions_model.ActionRunJob{JobID: "deploy", Needs: []string{"build"}, ParentJobID: 5} + jobs := []*actions_model.ActionRunJob{ + scoped, + {JobID: "build", Status: actions_model.StatusSuccess, ParentJobID: 0}, + } + assert.Equal(t, []string{"build"}, pendingNeeds(scoped, jobs)) + }) +} diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go index 6f2968691c..5e0e4b62c1 100644 --- a/routers/web/repo/attachment.go +++ b/routers/web/repo/attachment.go @@ -186,6 +186,22 @@ func ServeAttachment(ctx *context.Context, uuid string) { return } + // Draft release attachments must not be exposed to anyone without write + // access, matching the API-side canAccessReleaseDraft gate. Otherwise the + // UUID-based web endpoints would leak draft attachments to any recipient of + // the (leaked) download URL. + if unitType == unit.TypeReleases && attach.ReleaseID != 0 && !perm.CanWrite(unit.TypeReleases) { + rel, err := repo_model.GetReleaseByID(ctx, attach.ReleaseID) + if err != nil { + ctx.ServerError("GetReleaseByID", err) + return + } + if rel.IsDraft { + ctx.HTTPError(http.StatusNotFound) + return + } + } + if requiredScope, ok := attachmentReadScope(unitType); ok { context.CheckTokenScopes(ctx, repo, requiredScope) if ctx.Written() { diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index 457f9795a2..9b656ad0c1 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -13,7 +13,6 @@ import ( "strconv" "gitea.dev/models/gituser" - repo_model "gitea.dev/models/repo" "gitea.dev/modules/charset" "gitea.dev/modules/git" "gitea.dev/modules/git/languagestats" @@ -51,14 +50,14 @@ func RefBlame(ctx *context.Context) { ctx.NotFound(nil) return } - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx, ctx.Repo.GitRepo, ctx.Repo.TreePath) if err != nil { HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) return } - blob := entry.Blob() - fileSize := blob.Size() + blob := entry.Blob(ctx.Repo.GitRepo) + fileSize := blob.Size(ctx) ctx.Data["FileSize"] = fileSize ctx.Data["FileTreePath"] = ctx.Repo.TreePath @@ -74,14 +73,14 @@ func RefBlame(ctx *context.Context) { return } - ctx.Data["NumLines"], err = blob.GetBlobLineCount(nil) + _, ctx.Data["NumLines"], err = blob.GetBlobLineCount(ctx, nil) if err != nil { ctx.NotFound(err) return } bypassBlameIgnore, _ := strconv.ParseBool(ctx.FormString("bypass-blame-ignore")) - result, err := performBlame(ctx, ctx.Repo.Repository, ctx.Repo.Commit, ctx.Repo.TreePath, bypassBlameIgnore) + result, err := performBlame(ctx, bypassBlameIgnore) if err != nil { ctx.NotFound(err) return @@ -106,10 +105,14 @@ type blameResult struct { FaultyIgnoreRevsFile bool } -func performBlame(ctx *context.Context, repo *repo_model.Repository, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) { +func performBlame(ctx *context.Context, bypassBlameIgnore bool) (*blameResult, error) { + repo := ctx.Repo.Repository + gitRepo := ctx.Repo.GitRepo + commit := ctx.Repo.Commit + file := ctx.Repo.TreePath objectFormat := ctx.Repo.GetObjectFormat() - blameReader, err := gitrepo.CreateBlameReader(ctx, objectFormat, repo, commit, file, bypassBlameIgnore) + blameReader, err := gitrepo.CreateBlameReader(ctx, objectFormat, repo, gitRepo, commit, file, bypassBlameIgnore) if err != nil { return nil, err } @@ -125,7 +128,7 @@ func performBlame(ctx *context.Context, repo *repo_model.Repository, commit *git if len(r.Parts) == 0 && r.UsesIgnoreRevs { // try again without ignored revs - blameReader, err = gitrepo.CreateBlameReader(ctx, objectFormat, repo, commit, file, true) + blameReader, err = gitrepo.CreateBlameReader(ctx, objectFormat, repo, gitRepo, commit, file, true) if err != nil { return nil, err } @@ -194,7 +197,7 @@ func processBlameParts(ctx *context.Context, blameParts []*gitrepo.BlamePart) ma commit, ok := commitCache[sha] var err error if !ok { - commit, err = ctx.Repo.GitRepo.GetCommit(sha) + commit, err = ctx.Repo.GitRepo.GetCommit(ctx, sha) if err != nil { if git.IsErrNotExist(err) { ctx.NotFound(err) diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index fcd328efaf..ab91ab01f0 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -198,9 +198,9 @@ func CreateBranch(ctx *context.Context) { } err = release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, target, form.NewBranchName, "") } else if ctx.Repo.RefFullName.IsBranch() { - err = repo_service.CreateNewBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName) + err = repo_service.CreateNewBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.Repo.BranchName, form.NewBranchName) } else { - err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName) + err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.Repo.CommitID, form.NewBranchName) } if err != nil { if release_service.IsErrProtectedTagName(err) { diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index d895387818..c5108c39c4 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -76,7 +76,7 @@ func Commits(ctx *context.Context) { } // Both `git log branchName` and `git log commitId` work. - commits, err := ctx.Repo.Commit.CommitsByRange(page, pageSize, "", "", "") + commits, err := ctx.Repo.Commit.CommitsByRange(ctx, ctx.Repo.GitRepo, page, pageSize, "", "", "") if err != nil { ctx.ServerError("CommitsByRange", err) return @@ -141,7 +141,7 @@ func Graph(ctx *context.Context) { page := ctx.FormInt("page") - graph, err := gitgraph.GetCommitGraph(ctx.Repo.GitRepo, page, 0, hidePRRefs, realBranches, files) + graph, err := gitgraph.GetCommitGraph(ctx, ctx.Repo.GitRepo, page, 0, hidePRRefs, realBranches, files) if err != nil { ctx.ServerError("GetCommitGraph", err) return @@ -154,7 +154,7 @@ func Graph(ctx *context.Context) { ctx.Data["Graph"] = graph - gitRefs, err := ctx.Repo.GitRepo.GetRefs() + gitRefs, err := ctx.Repo.GitRepo.GetRefs(ctx) if err != nil { ctx.ServerError("GitRepo.GetRefs", err) return @@ -189,7 +189,7 @@ func SearchCommits(ctx *context.Context) { all := ctx.FormBool("all") opts := git.NewSearchCommitsOptions(query, all) - commits, err := ctx.Repo.Commit.SearchCommits(opts) + commits, err := ctx.Repo.Commit.SearchCommits(ctx, ctx.Repo.GitRepo, opts) if err != nil { ctx.ServerError("SearchCommits", err) return @@ -220,7 +220,7 @@ func FileHistory(ctx *context.Context) { ctx.Data["FollowRenameChecked"] = followRename page := max(ctx.FormInt("page"), 1) - commits, hasMore, err := ctx.Repo.GitRepo.CommitsByFileAndRange( + commits, hasMore, err := ctx.Repo.GitRepo.CommitsByFileAndRange(ctx, git.CommitsByFileAndRangeOptions{ Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName File: ctx.Repo.TreePath, @@ -302,7 +302,7 @@ func Diff(ctx *context.Context) { diffBlobExcerptData.BaseLink = ctx.Repo.RepoLink + "/wiki/blob_excerpt" } - commit, err := gitRepo.GetCommit(commitID) + commit, err := gitRepo.GetCommit(ctx, commitID) if err != nil { if git.IsErrNotExist(err) { ctx.NotFound(err) @@ -357,7 +357,7 @@ func Diff(ctx *context.Context) { var parentCommit *git.Commit var parentCommitID string if commit.ParentCount() > 0 { - parentCommit, err = gitRepo.GetCommit(parents[0]) + parentCommit, err = gitRepo.GetCommit(ctx, parents[0]) if err != nil { ctx.NotFound(err) return @@ -398,7 +398,7 @@ func Diff(ctx *context.Context) { verification := asymkey_service.ParseCommitWithSignature(ctx, commit) ctx.Data["Verification"] = verification ctx.Data["Author"] = user_model.GetUserByGitAuthor(ctx, commit) - ctx.Data["CommitOtherParticipants"] = gituser.BuildAvatarStackData(ctx, commit.AllParticipantIdentities(), nil).Participants[1:] + ctx.Data["CommitOtherParticipants"] = gituser.BuildAvatarStackData(ctx, commit.CoAuthorIdentities(), nil).Participants ctx.Data["Parents"] = parents ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0 @@ -433,7 +433,7 @@ func Diff(ctx *context.Context) { func RawDiff(ctx *context.Context) { var gitRepo *git.Repository if ctx.Data["PageIsWiki"] != nil { - wikiRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo()) + wikiRepo, err := gitrepo.OpenRepository(ctx.Repo.Repository.WikiStorageRepo()) if err != nil { ctx.ServerError("OpenRepository", err) return @@ -447,7 +447,7 @@ func RawDiff(ctx *context.Context) { return } } - if err := git.GetRawDiff( + if err := git.GetRawDiff(ctx, gitRepo, ctx.PathParam("sha"), git.RawDiffType(ctx.PathParam("ext")), diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 5a9fa1c1d1..5ee4aa7a74 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -10,7 +10,6 @@ import ( "io" "net/http" "net/url" - "path/filepath" "sort" "strings" "unicode" @@ -35,7 +34,6 @@ import ( "gitea.dev/modules/setting" api "gitea.dev/modules/structs" "gitea.dev/modules/templates" - "gitea.dev/modules/typesniffer" "gitea.dev/modules/util" "gitea.dev/routers/common" "gitea.dev/services/context" @@ -56,35 +54,7 @@ func setCompareContext(ctx *context.Context, before, head *git.Commit, headOwner ctx.Data["BeforeCommit"] = before ctx.Data["HeadCommit"] = head - ctx.Data["GetBlobByPathForCommit"] = func(commit *git.Commit, path string) *git.Blob { - if commit == nil { - return nil - } - - blob, err := commit.GetBlobByPath(path) - if err != nil { - return nil - } - return blob - } - - ctx.Data["GetSniffedTypeForBlob"] = func(blob *git.Blob) typesniffer.SniffedType { - st := typesniffer.SniffedType{} - - if blob == nil { - return st - } - - st, err := blob.GuessContentType() - if err != nil { - log.Error("GuessContentType failed: %v", err) - return st - } - return st - } - setPathsCompareContext(ctx, before, head, headOwner, headName) - setImageCompareContext(ctx) setCsvCompareContext(ctx) } @@ -108,20 +78,8 @@ func setPathsCompareContext(ctx *context.Context, base, head *git.Commit, headOw } } -// setImageCompareContext sets context data that is required by image compare template -func setImageCompareContext(ctx *context.Context) { - ctx.Data["IsSniffedTypeAnImage"] = func(st typesniffer.SniffedType) bool { - return st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()) - } -} - // setCsvCompareContext sets context data that is required by the CSV compare template func setCsvCompareContext(ctx *context.Context) { - ctx.Data["IsCsvFile"] = func(diffFile *gitdiff.DiffFile) bool { - extension := strings.ToLower(filepath.Ext(diffFile.Name)) - return extension == ".csv" || extension == ".tsv" - } - type CsvDiffResult struct { Sections []*gitdiff.TableDiffSection Error string @@ -140,11 +98,11 @@ func setCsvCompareContext(ctx *context.Context) { return nil, nil, nil } - if setting.UI.CSV.MaxFileSize != 0 && setting.UI.CSV.MaxFileSize < blob.Size() { + if setting.UI.CSV.MaxFileSize != 0 && setting.UI.CSV.MaxFileSize < blob.Size(ctx) { return nil, nil, errTooLarge } - reader, err := blob.DataAsync() + reader, err := blob.DataAsync(ctx) if err != nil { return nil, nil, err } @@ -208,11 +166,6 @@ func (cpi *comparePageInfoType) parseCompareInfo(ctx *context.Context, comparePa // 1 Parse compare router param compareReq := common.ParseCompareRouterParam(compareParam) - // remove the check when we support compare with carets - if compareReq.BaseOriRefSuffix != "" { - return util.NewInvalidArgumentErrorf("unsupported comparison syntax: ref with suffix") - } - // 2 get repository and owner for head headOwner, headRepo, err := common.GetHeadOwnerAndRepo(ctx, baseRepo, compareReq) if err != nil { @@ -241,18 +194,18 @@ func (cpi *comparePageInfoType) parseCompareInfo(ctx *context.Context, comparePa baseRefName := util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx)) headRefName := util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch) - baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefName) - if baseRef == "" { - return util.NewNotExistErrorf("no base ref: %s", baseRefName) + baseRef, err := common.ResolveRefWithSuffix(ctx, ctx.Repo.GitRepo, baseRefName, compareReq.BaseOriRefSuffix) + if err != nil { + return err } headGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, headRepo) if err != nil { return err } - headRef := headGitRepo.UnstableGuessRefByShortName(headRefName) - if headRef == "" { - return util.NewNotExistErrorf("no head ref: %s", headRefName) + headRef, err := common.ResolveRefWithSuffix(ctx, headGitRepo, headRefName, compareReq.HeadOriRefSuffix) + if err != nil { + return err } ctx.Data["BaseName"] = baseRepo.OwnerName @@ -388,7 +341,9 @@ func autoTitleFromBranchName(name string) string { func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*git_model.SignCommitWithStatuses, defaultTitleSource string) (title, content string) { useFirstCommitAsTitle := len(commits) == 1 || (defaultTitleSource == setting.RepoPRTitleSourceFirstCommit && len(commits) > 0) - if useFirstCommitAsTitle { + if defaultTitleSource == setting.RepoPRTitleSourceBranchName { + title = ci.HeadRef.ShortName() + } else if useFirstCommitAsTitle { // the "commits" are from "ShowPrettyFormatLogToList", which is ordered from newest to oldest, here take the oldest one c := commits[len(commits)-1] title = c.UserCommit.GitCommit.MessageTitle() @@ -499,7 +454,7 @@ func (cpi *comparePageInfoType) prepareCompareDiff(ctx *context.Context, whitesp ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML() } - headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID) + headCommit, err := ci.HeadGitRepo.GetCommit(ctx, headCommitID) if err != nil { ctx.ServerError("GetCommit", err) return @@ -507,7 +462,7 @@ func (cpi *comparePageInfoType) prepareCompareDiff(ctx *context.Context, whitesp baseGitRepo := ctx.Repo.GitRepo - beforeCommit, err := baseGitRepo.GetCommit(beforeCommitID) + beforeCommit, err := baseGitRepo.GetCommit(ctx, beforeCommitID) if err != nil { ctx.ServerError("GetCommit", err) return @@ -634,9 +589,9 @@ func downloadCompareDiffOrPatch(ctx *context.Context, patch bool) { var err error if patch { - err = ci.HeadGitRepo.GetPatch(compareArg, ctx.Resp) + err = ci.HeadGitRepo.GetPatch(ctx, compareArg, ctx.Resp) } else { - err = ci.HeadGitRepo.GetDiff(compareArg, ctx.Resp) + err = ci.HeadGitRepo.GetDiff(ctx, compareArg, ctx.Resp) } if err != nil { ctx.ServerError("DownloadCompareDiffOrPatch", err) @@ -771,17 +726,17 @@ func ExcerptBlob(ctx *context.Context) { diffBlobExcerptData.BaseLink = ctx.Repo.RepoLink + "/wiki/blob_excerpt" } - commit, err := gitRepo.GetCommit(commitID) + commit, err := gitRepo.GetCommit(ctx, commitID) if err != nil { ctx.ServerError("GetCommit", err) return } - blob, err := commit.Tree.GetBlobByPath(filePath) + blob, err := commit.GetBlobByPath(ctx, ctx.Repo.GitRepo, filePath) if err != nil { ctx.ServerError("GetBlobByPath", err) return } - reader, err := blob.DataAsync() + reader, err := blob.DataAsync(ctx) if err != nil { ctx.ServerError("DataAsync", err) return diff --git a/routers/web/repo/compare_test.go b/routers/web/repo/compare_test.go index af0a735227..d5b7609fa3 100644 --- a/routers/web/repo/compare_test.go +++ b/routers/web/repo/compare_test.go @@ -70,6 +70,10 @@ func TestNewPullRequestTitleContent(t *testing.T) { assert.Equal(t, "Head branch", title) assert.Empty(t, content) + title, content = prepareNewPullRequestTitleContent(ci, nil, setting.RepoPRTitleSourceBranchName) + assert.Equal(t, "head-branch", title) + assert.Empty(t, content) + // single commit title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("single-commit-title\nbody")}, setting.RepoPRTitleSourceAuto) assert.Equal(t, "single-commit-title", title) diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go index e92e44532a..ed6de00092 100644 --- a/routers/web/repo/download.go +++ b/routers/web/repo/download.go @@ -30,7 +30,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim return nil } - lfsPointerBuf, err := blob.GetBlobBytes(lfs.MetaFileMaxSize) + lfsPointerBuf, err := blob.GetBlobBytes(ctx, lfs.MetaFileMaxSize) if err != nil { return err } @@ -67,7 +67,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim } func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) { - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx, ctx.Repo.GitRepo, ctx.Repo.TreePath) if err != nil { if git.IsErrNotExist(err) { ctx.NotFound(err) @@ -82,14 +82,14 @@ func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) { return nil, nil } - latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath) + latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx, ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath) if err != nil { ctx.ServerError("GetTreePathLatestCommit", err) return nil, nil } lastModified := &latestCommit.Committer.When - return entry.Blob(), lastModified + return entry.Blob(ctx.Repo.GitRepo), lastModified } // SingleDownload download a file by repos path diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 95928d8703..187ed837fe 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -224,7 +224,7 @@ func redirectForCommitChoice[T any](ctx *context.Context, parsed *preparedEditor } func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) { - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx, ctx.Repo.GitRepo, ctx.Repo.TreePath) if err != nil { HandleGitError(ctx, "GetTreeEntryByPath", err) return nil, nil, nil @@ -236,7 +236,7 @@ func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.Read return nil, nil, nil } - blob := entry.Blob() + blob := entry.Blob(ctx.Repo.GitRepo) buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) if err != nil { if git.IsErrNotExist(err) { @@ -403,7 +403,7 @@ func DeleteFilePost(ctx *context.Context) { } // Check if the path is a directory - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath) + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx, ctx.Repo.GitRepo, treePath) if err != nil { ctx.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err) return @@ -442,7 +442,7 @@ func DeleteFilePost(ctx *context.Context) { } else { ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath)) } - redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath) + redirectTreePath := getClosestParentWithFiles(ctx, ctx.Repo.GitRepo, parsed.NewBranchName, treePath) redirectForCommitChoice(ctx, parsed, redirectTreePath) } diff --git a/routers/web/repo/editor_cherry_pick.go b/routers/web/repo/editor_cherry_pick.go index 18138d32cd..12c0bdb0b5 100644 --- a/routers/web/repo/editor_cherry_pick.go +++ b/routers/web/repo/editor_cherry_pick.go @@ -23,7 +23,7 @@ func CherryPick(ctx *context.Context) { fromCommitID := ctx.PathParam("sha") ctx.Data["FromCommitID"] = fromCommitID - cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(fromCommitID) + cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(ctx, fromCommitID) if err != nil { HandleGitError(ctx, "GetCommit", err) return @@ -66,7 +66,7 @@ func CherryPickPost(ctx *context.Context) { if parsed.form.Revert { err = gitrepo.GetReverseRawDiff(ctx, ctx.Repo.Repository, fromCommitID, buf) } else { - err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, git.RawDiffPatch, buf) + err = git.GetRawDiff(ctx, ctx.Repo.GitRepo, fromCommitID, git.RawDiffPatch, buf) } if err == nil { opts.Content = buf.String() diff --git a/routers/web/repo/editor_preview.go b/routers/web/repo/editor_preview.go index 5fc0d38cca..5b4fb5bf74 100644 --- a/routers/web/repo/editor_preview.go +++ b/routers/web/repo/editor_preview.go @@ -19,7 +19,7 @@ func DiffPreviewPost(ctx *context.Context) { return } - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath) + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx, ctx.Repo.GitRepo, treePath) if err != nil { ctx.ServerError("GetTreeEntryByPath", err) return @@ -28,7 +28,7 @@ func DiffPreviewPost(ctx *context.Context) { return } - oldContent, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize) + oldContent, err := entry.Blob(ctx.Repo.GitRepo).GetBlobContent(ctx, setting.UI.MaxDisplayFileSize) if err != nil { ctx.ServerError("GetBlobContent", err) return diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go index 73fc92e8a1..fc0a38ce33 100644 --- a/routers/web/repo/editor_test.go +++ b/routers/web/repo/editor_test.go @@ -21,11 +21,11 @@ func TestEditorUtils(t *testing.T) { assert.Equal(t, "user2-patch-1", branchName) }) t.Run("getClosestParentWithFiles", func(t *testing.T) { - gitRepo, _ := gitrepo.OpenRepository(t.Context(), repo) + gitRepo, _ := gitrepo.OpenRepository(repo) defer gitRepo.Close() - treePath := getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "docs/foo/bar") + treePath := getClosestParentWithFiles(t.Context(), gitRepo, "sub-home-md-img-check", "docs/foo/bar") assert.Equal(t, "docs", treePath) - treePath = getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "any/other") + treePath = getClosestParentWithFiles(t.Context(), gitRepo, "sub-home-md-img-check", "any/other") assert.Empty(t, treePath) }) } diff --git a/routers/web/repo/editor_util.go b/routers/web/repo/editor_util.go index c23eee18fc..523d4547c3 100644 --- a/routers/web/repo/editor_util.go +++ b/routers/web/repo/editor_util.go @@ -43,21 +43,21 @@ func getUniquePatchBranchName(ctx context.Context, prefixName string, repo *repo // getClosestParentWithFiles Recursively gets the closest path of parent in a tree that has files when a file in a tree is // deleted. It returns "" for the tree root if no parents other than the root have files. -func getClosestParentWithFiles(gitRepo *git.Repository, branchName, originTreePath string) string { +func getClosestParentWithFiles(ctx context.Context, gitRepo *git.Repository, branchName, originTreePath string) string { var f func(treePath string, commit *git.Commit) string f = func(treePath string, commit *git.Commit) string { if treePath == "" || treePath == "." { return "" } // see if the tree has entries - if tree, err := commit.SubTree(treePath); err != nil { + if tree, err := commit.SubTree(ctx, gitRepo, treePath); err != nil { return f(path.Dir(treePath), commit) // failed to get the tree, going up a dir - } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 { + } else if entries, err := tree.ListEntries(ctx, gitRepo); err != nil || len(entries) == 0 { return f(path.Dir(treePath), commit) // no files in this dir, going up a dir } return treePath } - commit, err := gitRepo.GetBranchCommit(branchName) // must get the commit again to get the latest change + commit, err := gitRepo.GetBranchCommit(ctx, branchName) // must get the commit again to get the latest change if err != nil { log.Error("GetBranchCommit: %v", err) return "" @@ -87,7 +87,7 @@ func getCodeEditorConfigByEditorconfig(ctx *context_service.Context, treePath st ret.LineWrapExtensions = setting.Repository.Editor.LineWrapExtensions ret.LineWrap = util.SliceContainsString(ret.LineWrapExtensions, path.Ext(treePath), true) ret.Previewable = util.SliceContainsString(ret.PreviewableExtensions, path.Ext(treePath), true) - ec, _, err := ctx.Repo.GetEditorconfig() + ec, _, err := ctx.Repo.GetEditorconfig(ctx) if err == nil { def, err := ec.GetDefinitionForFilename(treePath) if err == nil { diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index 4ae2955f6d..15577e245c 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -418,7 +418,8 @@ func serviceRPC(ctx *context.Context, service string) { WithStdoutCopy(ctx.Resp), ); err != nil { if !gitcmd.IsErrorCanceledOrKilled(err) { - log.Error("Fail to serve RPC(%s) in %s: %v", service, h.getStorageRepo().RelativePath(), err) + repoLogName := h.repo.FullName() + util.Iif(h.isWiki, ".wiki", "") + log.Error("Fail to serve RPC(%s) for repo %s: %v", service, repoLogName, err) } } } diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go index 78437b3736..7570147fc4 100644 --- a/routers/web/repo/issue_dependency.go +++ b/routers/web/repo/issue_dependency.go @@ -130,6 +130,25 @@ func RemoveDependency(ctx *context.Context) { return } + // Existing cross-repo dependencies must remain removable even when + // AllowCrossRepositoryDependencies is disabled, so only enforce that the + // doer can read the dependency's repository. + if issue.RepoID != dep.RepoID { + if err := dep.LoadRepo(ctx); err != nil { + ctx.ServerError("loadRepo", err) + return + } + depRepoPerm, err := access_model.GetDoerRepoPermission(ctx, dep.Repo, ctx.Doer) + if err != nil { + ctx.ServerError("GetDoerRepoPermission", err) + return + } + if !depRepoPerm.CanReadIssuesOrPulls(dep.IsPull) { + ctx.Redirect(issue.Link()) + return + } + } + if err = issues_model.RemoveIssueDependency(ctx, ctx.Doer, issue, dep, depType); err != nil { if issues_model.IsErrDependencyNotExists(err) { ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_exist")) diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go index cef75f4bbc..f926234814 100644 --- a/routers/web/repo/issue_label.go +++ b/routers/web/repo/issue_label.go @@ -179,9 +179,11 @@ func UpdateIssueLabel(ctx *context.Context) { } } case "attach", "detach", "toggle", "toggle-alt": - label, err := issues_model.GetLabelByID(ctx, ctx.FormInt64("id")) + // scope the label to this repo (or its org) so a foreign label ID is 404, not an oracle + labelID := ctx.FormInt64("id") + label, err := issues_model.GetLabelInRepoOrOrgByID(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner.ID, ctx.Repo.Owner.IsOrganization(), labelID) if err != nil { - if issues_model.IsErrRepoLabelNotExist(err) { + if errors.Is(err, util.ErrNotExist) { ctx.HTTPError(http.StatusNotFound, "GetLabelByID") } else { ctx.ServerError("GetLabelByID", err) diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index 5ccef292a2..9bdc6822f2 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -754,7 +754,7 @@ func Issues(ctx *context.Context) { } ctx.Data["Title"] = ctx.Tr("repo.issues") ctx.Data["PageIsIssueList"] = true - ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) + ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo) } projectIDs := parseProjectIDsFromQuery(ctx) diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go index 57786dd7d7..1b64104eb4 100644 --- a/routers/web/repo/issue_new.go +++ b/routers/web/repo/issue_new.go @@ -38,7 +38,7 @@ import ( // Tries to load and set an issue template. The first return value indicates if a template was loaded. func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string, metaData *IssuePageMetaData) (bool, map[string]error) { - commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) + commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx, ctx.Repo.Repository.DefaultBranch) if err != nil { return false, nil } @@ -51,10 +51,10 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles templateErrs := map[string]error{} for _, filename := range templateCandidates { - if ok, _ := commit.HasFile(filename); !ok { + template, err := issue_template.UnmarshalFromCommit(ctx, ctx.Repo.GitRepo, commit, filename) + if errors.Is(err, util.ErrNotExist) { continue } - template, err := issue_template.UnmarshalFromCommit(commit, filename) if err != nil { templateErrs[filename] = err continue @@ -98,8 +98,8 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles // NewIssue render creating issue page func NewIssue(ctx *context.Context) { - issueConfig, _ := issue_service.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) - hasTemplates := issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) + issueConfig, _ := issue_service.GetTemplateConfigFromDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo) + hasTemplates := issue_service.HasTemplatesOrContactLinks(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo) ctx.Data["Title"] = ctx.Tr("repo.issues.new") ctx.Data["PageIsIssueList"] = true @@ -134,7 +134,7 @@ func NewIssue(ctx *context.Context) { } ctx.Data["Tags"] = tags - ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) + ret := issue_service.ParseTemplatesFromDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo) templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, pageMetaData) maps.Copy(ret.TemplateErrors, errs) if ctx.Written() { @@ -186,20 +186,20 @@ func NewIssueChooseTemplate(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.issues.new") ctx.Data["PageIsIssueList"] = true - ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) + ret := issue_service.ParseTemplatesFromDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo) ctx.Data["IssueTemplates"] = ret.IssueTemplates if len(ret.TemplateErrors) > 0 { ctx.Flash.Warning(renderErrorOfTemplates(ctx, ret.TemplateErrors), true) } - if !issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) { + if !issue_service.HasTemplatesOrContactLinks(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo) { // The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if no template here, just redirect to the "issues/new" page with these parameters. ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther) return } - issueConfig, err := issue_service.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) + issueConfig, err := issue_service.GetTemplateConfigFromDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo) ctx.Data["IssueConfig"] = issueConfig ctx.Data["IssueConfigError"] = err // ctx.Flash.Err makes problems here @@ -358,7 +358,7 @@ func NewIssuePost(ctx *context.Context) { content := form.Content if filename := ctx.Req.Form.Get("template-file"); filename != "" { - if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil { + if template, err := issue_template.UnmarshalFromRepo(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil { content = issue_template.RenderToMarkdown(template, ctx.Req.Form) } } diff --git a/routers/web/repo/issue_poster.go b/routers/web/repo/issue_poster.go index 09c941509f..683db527b6 100644 --- a/routers/web/repo/issue_poster.go +++ b/routers/web/repo/issue_poster.go @@ -14,17 +14,6 @@ import ( "gitea.dev/services/context" ) -type userSearchInfo struct { - UserID int64 `json:"user_id"` - UserName string `json:"username"` - AvatarLink string `json:"avatar_link"` - FullName string `json:"full_name"` -} - -type userSearchResponse struct { - Results []*userSearchInfo `json:"results"` -} - func IssuePullPosters(ctx *context.Context) { isPullList := ctx.PathParam("type") == "pulls" issuePosters(ctx, isPullList) @@ -46,14 +35,5 @@ func issuePosters(ctx *context.Context, isPullList bool) { posters = append(posters, ctx.Doer) } } - - posters = shared_user.MakeSelfOnTop(ctx.Doer, posters) - - resp := &userSearchResponse{} - resp.Results = make([]*userSearchInfo, len(posters)) - for i, user := range posters { - resp.Results[i] = &userSearchInfo{UserID: user.ID, UserName: user.Name, AvatarLink: user.AvatarLink(ctx)} - resp.Results[i].FullName = user.FullName - } - ctx.JSON(http.StatusOK, resp) + ctx.JSON(http.StatusOK, shared_user.ToSearchUserResponse(ctx, ctx.Doer, posters)) } diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index ac5a3beb03..f78d04efa9 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -334,7 +334,7 @@ func ViewIssue(ctx *context.Context) { return } ctx.Data["PageIsIssueList"] = true - ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) + ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo) } ctx.Data["IsProjectsEnabled"] = ctx.Repo.Permission.CanRead(unit.TypeProjects) @@ -418,7 +418,7 @@ func ViewIssue(ctx *context.Context) { return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee) } - if !setting.IsProd && issue.PullRequest != nil && !issue.PullRequest.IsChecking() && prViewInfo.MergeBoxData != nil { + if !setting.IsProd && issue.PullRequest != nil && prViewInfo.MergeBoxData != nil && prViewInfo.MergeBoxData.ReloadingInterval == 0 { prViewInfo.MergeBoxData.ReloadingInterval = 1 // in dev env, force using the reloading logic to make sure it won't break } @@ -495,7 +495,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxCommitSigning(ctx *context.Con wontSignReason := "" if ctx.Doer != nil { - sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, ctx.Repo.GitRepo) + sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, ctx.Repo.GitRepo, pull.BaseBranch, pull.GetGitHeadRefName()) data.willSign = sign data.signingKeyMergeDisplay = asymkey_model.GetDisplaySigningKey(key) if err != nil { @@ -919,7 +919,6 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue * } } - data.ReloadingInterval = util.Iif(pull.IsChecking(), 2000, 0) data.ShowMergeInstructions = canWriteToHeadRepo data.ShowPullCommands = pull.HeadRepo != nil && !pull.HasMerged && !issue.IsClosed @@ -941,6 +940,10 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue * prConfig := issue.Repo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig() data.AutodetectManualMerge = prConfig.AutodetectManualMerge + needRefreshMergeBox := pull.IsChecking() + needRefreshMergeBox = needRefreshMergeBox || (data.StatusCheckData != nil && data.StatusCheckData.pullCommitStatusState.IsPending()) + data.ReloadingInterval = util.Iif(needRefreshMergeBox, 5000, 0) + // Only show the merge box if the PR is not merged, or the branch is deletable. // Otherwise, there is nothing to do, because the PR view page already contains enough information. data.ShowMergeBox = !pull.HasMerged || data.IsPullBranchDeletable @@ -949,7 +952,9 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue * // admin can merge without checks, writer can merge when checks succeed // admin and writer both can make an auto merge schedule (not affected by overridable blockers) - data.hasStatusCheckBlocker = data.enableStatusCheck && !data.StatusCheckData.RequiredChecksState.IsSuccess() + // Required scoped workflow checks gate the merge even when the rule's own status check is disabled (see IsPullCommitStatusPass), + // so block on any required status context, not only when enableStatusCheck is on. + data.hasStatusCheckBlocker = (data.enableStatusCheck || data.hasRequiredStatusContexts) && !data.StatusCheckData.RequiredChecksState.IsSuccess() // this logic is from: // {{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection .IsBlockedByOfficialReviewRequests .IsBlockedByOutdatedBranch .IsBlockedByChangedProtectedFiles (and .EnableStatusCheck (not $requiredStatusCheckState.IsSuccess))}} diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go index 8ae99fbbb7..a4c0fac551 100644 --- a/routers/web/repo/middlewares.go +++ b/routers/web/repo/middlewares.go @@ -22,7 +22,7 @@ func SetEditorconfigIfExists(ctx *context.Context) { return } - ec, _, err := ctx.Repo.GetEditorconfig() + ec, _, err := ctx.Repo.GetEditorconfig(ctx) if err != nil { // it used to check `!git.IsErrNotExist(err)` and create a system notice, but it is quite annoying and useless // because network errors also happen frequently, so we just ignore it diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index ad3a71804c..c639754dc8 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -262,7 +262,7 @@ func MilestoneIssuesAndPulls(ctx *context.Context) { prepareIssueFilterAndList(ctx, milestoneID, projectIDs, optional.None[bool]()) - ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) + ret := issue.ParseTemplatesFromDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo) ctx.Data["NewIssueChooseTemplate"] = len(ret.IssueTemplates) > 0 ctx.Data["CanWriteIssues"] = ctx.Repo.Permission.CanWriteIssuesOrPulls(false) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 81283a2538..379a011121 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -203,7 +203,7 @@ func GetPullDiffStats(ctx *context.Context) { } // do not report 500 server error to end users if error occurs, otherwise a PR missing ref won't be able to view. - headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitHeadRefName()) + headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(ctx, pull.GetGitHeadRefName()) if errors.Is(err, util.ErrNotExist) { return } else if err != nil { @@ -227,7 +227,7 @@ func GetMergedBaseCommitID(ctx *context.Context, issue *issues_model.Issue) stri if pull.MergeBase == "" { var commitSHA, parentCommit string // If there is a head or a patch file, and it is readable, grab info - commitSHA, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitHeadRefName()) + commitSHA, err := ctx.Repo.GitRepo.GetRefCommitID(ctx, pull.GetGitHeadRefName()) if err != nil { // Head File does not exist, try the patch commitSHA, err = ctx.Repo.GitRepo.ReadPatchCommit(pull.Index) @@ -276,6 +276,10 @@ type pullMergeBoxData struct { enableStatusCheck bool StatusCheckData *pullCommitStatusCheckData ShowStatusCheck bool + // hasRequiredStatusContexts is true when at least one required status-check context must be satisfied: + // the branch protection's own contexts and/or required scoped workflow checks. + // The latter gate the merge even when the rule's own status check is disabled. + hasRequiredStatusContexts bool hasOverridableBlockers bool canMergeNow bool // PR is mergeable, either no blocker, or doer can bypass the blockers @@ -423,6 +427,16 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxStatusCheckData(ctx *context.C if err != nil { log.Error("GetLatestCommitStatus: %v", err) } + + // Effective required contexts = branch-protection contexts + required scoped workflow checks. + requiredContexts := pbRequiredContexts + if effective, err := pull_service.EffectiveRequiredContexts(ctx, ctx.Repo.Repository, prInfo.ProtectedBranchRule); err != nil { + log.Error("EffectiveRequiredContexts: %v", err) + } else { + requiredContexts = effective + } + data.hasRequiredStatusContexts = len(requiredContexts) > 0 + if !ctx.Repo.Permission.CanRead(unit.TypeActions) { git_model.CommitStatusesHideActionsURL(ctx, commitStatuses) } @@ -433,7 +447,9 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxStatusCheckData(ctx *context.C statusCheckData.pullCommitStatusState = combinedCommitStatus.State } - data.ShowStatusCheck = data.enableStatusCheck || len(statusCheckData.PullCommitStatuses) > 0 + // Required scoped workflow checks gate the merge even when the branch protection's own status check is disabled, + // so the status-check section must render when there are any required contexts, not only when enableStatusCheck is on. + data.ShowStatusCheck = data.enableStatusCheck || data.hasRequiredStatusContexts || len(statusCheckData.PullCommitStatuses) > 0 runs, err := actions_service.GetRunsFromCommitStatuses(ctx, commitStatuses) if err != nil { @@ -449,7 +465,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxStatusCheckData(ctx *context.C } var missingRequiredChecks []string - for _, requiredContext := range pbRequiredContexts { + for _, requiredContext := range requiredContexts { contextFound := false matchesRequiredContext := createRequiredContextMatcher(requiredContext) for _, presentStatus := range commitStatuses { @@ -466,7 +482,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxStatusCheckData(ctx *context.C statusCheckData.MissingRequiredChecks = missingRequiredChecks statusCheckData.IsContextRequired = func(context string) bool { - for _, c := range pbRequiredContexts { + for _, c := range requiredContexts { if c == context { return true } @@ -481,9 +497,9 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxStatusCheckData(ctx *context.C } return false } - statusCheckData.RequiredChecksState = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pbRequiredContexts) + statusCheckData.RequiredChecksState = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, requiredContexts) - if data.enableStatusCheck { + if data.enableStatusCheck || data.hasRequiredStatusContexts { if statusCheckData.RequiredChecksState.IsError() || statusCheckData.RequiredChecksState.IsFailure() { data.infoProtectionBlockers.AddErrorItem(ctx.Locale.Tr("repo.pulls.required_status_check_failed")) } else if !statusCheckData.RequiredChecksState.IsSuccess() { @@ -539,13 +555,13 @@ func getViewPullHeadBranchCommitID(ctx *context.Context, pull *issues_model.Pull if err != nil { return "", err } - return headGitRepo.GetRefCommitID(git.RefNameFromBranch(pull.HeadBranch).String()) + return headGitRepo.GetRefCommitID(ctx, git.RefNameFromBranch(pull.HeadBranch).String()) case issues_model.PullRequestFlowAGit: baseGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, pull.BaseRepo) if err != nil { return "", err } - return baseGitRepo.GetRefCommitID(pull.GetGitHeadRefName()) + return baseGitRepo.GetRefCommitID(ctx, pull.GetGitHeadRefName()) } setting.PanicInDevOrTesting("invalid pull request flow type: %v", pull.Flow) return "", util.ErrNotExist @@ -715,7 +731,7 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) { afterCommitID = util.IfZero(afterCommitID, headCommitID) afterCommit := indexCommit(prCompareInfo.Commits, afterCommitID) if afterCommit == nil && afterCommitID == headCommitID { - afterCommit, err = gitRepo.GetCommit(afterCommitID) + afterCommit, err = gitRepo.GetCommit(ctx, afterCommitID) if err != nil { ctx.ServerError("GetCommit(afterCommitID)", err) return @@ -728,7 +744,7 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) { var beforeCommit *git.Commit if isSingleCommit { - beforeCommit, err = afterCommit.Parent(0) + beforeCommit, err = afterCommit.Parent(ctx, ctx.Repo.GitRepo, 0) if err != nil { ctx.ServerError("afterCommit.Parent", err) return @@ -739,7 +755,7 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) { beforeCommit = indexCommit(prCompareInfo.Commits, beforeCommitID) if beforeCommit == nil && beforeCommitID == prCompareInfo.CompareBase { // base commit is not in the list of the pull request commits - beforeCommit, err = gitRepo.GetCommit(beforeCommitID) + beforeCommit, err = gitRepo.GetCommit(ctx, beforeCommitID) if err != nil { ctx.ServerError("GetCommit(beforeCommitID)", err) return @@ -1361,7 +1377,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { content := form.Content if filename := ctx.Req.Form.Get("template-file"); filename != "" { - if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil { + if template, err := issue_template.UnmarshalFromRepo(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil { content = issue_template.RenderToMarkdown(template, ctx.Req.Form) } } diff --git a/routers/web/repo/pull_merge_box.go b/routers/web/repo/pull_merge_box.go index 4a167c2912..d76aacde2f 100644 --- a/routers/web/repo/pull_merge_box.go +++ b/routers/web/repo/pull_merge_box.go @@ -63,7 +63,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxIconColor() { showAsWarningColor = showAsWarningColor || statusCheckData.pullCommitStatusState.IsWarning() || statusCheckData.pullCommitStatusState.IsPending() || - (mergeBoxData.enableStatusCheck && (statusCheckData.RequiredChecksState.IsWarning() || statusCheckData.RequiredChecksState.IsPending())) + ((mergeBoxData.enableStatusCheck || mergeBoxData.hasRequiredStatusContexts) && (statusCheckData.RequiredChecksState.IsWarning() || statusCheckData.RequiredChecksState.IsPending())) } hasBlockers := len(mergeBoxData.infoCommitBlockers.items) > 0 || len(mergeBoxData.infoProtectionBlockers.items) > 0 diff --git a/routers/web/repo/pull_merge_form.go b/routers/web/repo/pull_merge_form.go index 668717604f..fe12aa6105 100644 --- a/routers/web/repo/pull_merge_form.go +++ b/routers/web/repo/pull_merge_form.go @@ -10,6 +10,8 @@ import ( pull_model "gitea.dev/models/pull" repo_model "gitea.dev/models/repo" "gitea.dev/models/unit" + "gitea.dev/modules/git" + "gitea.dev/modules/log" "gitea.dev/modules/svg" "gitea.dev/modules/templates" "gitea.dev/modules/util" @@ -62,20 +64,23 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context hasPendingPullRequestMergeTip = ctx.Locale.Tr("repo.pulls.auto_merge_has_pending_schedule", pendingPullRequestMerge.Doer.Name, createdPRMergeStr) } - defaultMergeTitle, defaultMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle) - if err != nil && !errors.Is(err, util.ErrNotExist) { - ctx.ServerError("GetDefaultMergeMessage", err) - return - } - defaultSquashMergeTitle, defaultSquashMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash) - if err != nil && !errors.Is(err, util.ErrNotExist) { - ctx.ServerError("GetDefaultSquashMergeMessage", err) - return - } - + var defaultMergeTitle, defaultMergeBody string + var defaultSquashMergeTitle, defaultSquashMergeBody string var defaultSquashMergeCommitMessages string if !prInfo.IsPullRequestBroken { - defaultSquashMergeCommitMessages = pull_service.GetSquashMergeCommitMessages(ctx, pull) + var err error + defaultMergeTitle, defaultMergeBody, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle) + if err != nil && !errors.Is(err, util.ErrNotExist) { + log.Error("GetDefaultMergeMessage for style %s failed, error: %v", mergeStyle, err) + } + defaultSquashMergeTitle, defaultSquashMergeBody, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash) + if err != nil && !errors.Is(err, util.ErrNotExist) { + log.Error("GetDefaultMergeMessage for squash failed, error: %v", err) + } + defaultSquashMergeCommitMessages, err = pull_service.GetSquashMergeCommitMessages(ctx, pull) + if err != nil && !errors.Is(err, util.ErrNotExist) { + log.Error("GetSquashMergeCommitMessages failed, error: %v", err) + } } allOverridableChecksOk := !prInfo.MergeBoxData.hasOverridableBlockers @@ -106,7 +111,6 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context // if this pr can be merged now, then hide the auto merge generalHideAutoMerge := prInfo.MergeBoxData.canMergeNow && allOverridableChecksOk - var mergeStyles []any if pull.IsStatusMergeable() { mergeStyles = []any{ @@ -138,7 +142,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context "allowed": prConfig.AllowSquash, "textDoMerge": ctx.Locale.Tr("repo.pulls.squash_merge_pull_request"), "mergeTitleFieldText": defaultSquashMergeTitle, - "mergeMessageFieldText": defaultSquashMergeCommitMessages + defaultSquashMergeBody, + "mergeMessageFieldText": git.CommitMessageMerge(defaultSquashMergeCommitMessages, defaultSquashMergeBody), "hideAutoMerge": generalHideAutoMerge, }, map[string]any{ diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go index b71d4d27a9..2ebd9bd63c 100644 --- a/routers/web/repo/pull_review.go +++ b/routers/web/repo/pull_review.go @@ -49,7 +49,7 @@ func RenderNewCodeCommentForm(ctx *context.Context) { ctx.Data["PageIsPullFiles"] = true ctx.Data["Issue"] = issue ctx.Data["CurrentReview"] = currentReview - pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(issue.PullRequest.GetGitHeadRefName()) + pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(ctx, issue.PullRequest.GetGitHeadRefName()) if err != nil { ctx.ServerError("GetRefCommitID", err) return @@ -199,7 +199,7 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori ctx.ServerError("comment.Issue.LoadPullRequest", err) return } - pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(comment.Issue.PullRequest.GetGitHeadRefName()) + pullHeadCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(ctx, comment.Issue.PullRequest.GetGitHeadRefName()) if err != nil { ctx.ServerError("GetRefCommitID", err) return diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 5e267cc113..a5aac43ff4 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -27,7 +27,6 @@ import ( "gitea.dev/modules/util" "gitea.dev/modules/web" "gitea.dev/routers/web/feed" - shared_user "gitea.dev/routers/web/shared/user" "gitea.dev/services/context" "gitea.dev/services/context/upload" "gitea.dev/services/forms" @@ -48,7 +47,7 @@ func calReleaseNumCommitsBehind(ctx stdCtx.Context, repoCtx *context.Repository, } // Get count if not cached if _, ok := countCache[target]; !ok { - commit, err := repoCtx.GitRepo.GetBranchCommit(target) + commit, err := repoCtx.GitRepo.GetBranchCommit(ctx, target) if err != nil { var errNotExist git.ErrNotExist if target == repoCtx.Repository.DefaultBranch || !errors.As(err, &errNotExist) { @@ -56,7 +55,7 @@ func calReleaseNumCommitsBehind(ctx stdCtx.Context, repoCtx *context.Repository, } // fallback to default branch target = repoCtx.Repository.DefaultBranch - commit, err = repoCtx.GitRepo.GetBranchCommit(target) + commit, err = repoCtx.GitRepo.GetBranchCommit(ctx, target) if err != nil { return fmt.Errorf("GetBranchCommit(DefaultBranch): %w", err) } @@ -101,6 +100,19 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) canReadActions := ctx.Repo.Permission.CanRead(unit.TypeActions) + // Bulk-load commit statuses for all releases in one query. + var commitStatusMap map[string][]*git_model.CommitStatus + if canReadActions && len(releases) > 0 { + shas := make([]string, 0, len(releases)) + for _, r := range releases { + shas = append(shas, r.Sha1) + } + commitStatusMap, err = git_model.GetLatestCommitStatusForRepoCommitIDs(ctx, ctx.Repo.Repository.ID, shas) + if err != nil { + return nil, err + } + } + releaseInfos := make([]*ReleaseInfo, 0, len(releases)) for _, r := range releases { if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok { @@ -130,11 +142,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) } if canReadActions { - statuses, err := git_model.GetLatestCommitStatus(ctx, r.Repo.ID, r.Sha1, db.ListOptionsAll) - if err != nil { - return nil, err - } - + statuses := commitStatusMap[r.Sha1] info.CommitStatus = git_model.CalcCommitStatus(statuses) info.CommitStatuses = statuses } @@ -329,7 +337,6 @@ func LatestRelease(ctx *context.Context) { func newReleaseCommon(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.release.new_release") - ctx.Data["PageIsReleaseList"] = true tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) if err != nil { @@ -337,17 +344,8 @@ func newReleaseCommon(ctx *context.Context) { return } ctx.Data["Tags"] = tags - ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled - assigneeUsers, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository) - if err != nil { - ctx.ServerError("GetRepoAssignees", err) - return - } - ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers) - upload.AddUploadContext(ctx, "release") - PrepareBranchList(ctx) // for New Release page } @@ -416,8 +414,8 @@ func GenerateReleaseNotes(ctx *context.Context) { // NewReleasePost response for creating a release func NewReleasePost(ctx *context.Context) { - newReleaseCommon(ctx) - if ctx.Written() { + if ctx.HasError() { + ctx.JSONError(ctx.GetErrMsg()) return } @@ -441,35 +439,28 @@ func NewReleasePost(ctx *context.Context) { // Or another choice is "always show the tag-only button" if error occurs. ctx.Data["ShowCreateTagOnlyButton"] = form.TagOnly || rel == nil - // do some form checks - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplReleaseNew) - return - } - form.Target = util.IfZero(form.Target, ctx.Repo.Repository.DefaultBranch) if exist, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, form.Target); !exist { - ctx.RenderWithErrDeprecated(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form) + ctx.JSONError(ctx.Tr("form.target_branch_not_exist")) return } if !form.TagOnly && form.Title == "" { // if not "tag only", then the title of the release cannot be empty - ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.title_empty"), tplReleaseNew, &form) + ctx.JSONError(ctx.Tr("repo.release.title_empty")) return } handleTagReleaseError := func(err error) { - ctx.Data["Err_TagName"] = true switch { case release_service.IsErrTagAlreadyExists(err): - ctx.RenderWithErrDeprecated(ctx.Tr("repo.branch.tag_collision", form.TagName), tplReleaseNew, &form) + ctx.JSONError(ctx.Tr("repo.branch.tag_collision", form.TagName)) case repo_model.IsErrReleaseAlreadyExist(err): - ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) + ctx.JSONError(ctx.Tr("repo.release.tag_name_already_exist")) case release_service.IsErrInvalidTagName(err): - ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form) + ctx.JSONError(ctx.Tr("repo.release.tag_name_invalid")) case release_service.IsErrProtectedTagName(err): - ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form) + ctx.JSONError(ctx.Tr("repo.release.tag_name_protected")) default: ctx.ServerError("handleTagReleaseError", err) } @@ -488,7 +479,7 @@ func NewReleasePost(ctx *context.Context) { return } ctx.Flash.Success(ctx.Tr("repo.tag.create_success", form.TagName)) - ctx.Redirect(ctx.Repo.RepoLink + "/src/tag/" + util.PathEscapeSegments(form.TagName)) + ctx.JSONRedirect(ctx.Repo.RepoLink + "/src/tag/" + util.PathEscapeSegments(form.TagName)) return } @@ -509,11 +500,11 @@ func NewReleasePost(ctx *context.Context) { IsPrerelease: form.Prerelease, IsTag: false, } - if err = release_service.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs, newTagMsg); err != nil { + if err = release_service.CreateRelease(ctx, ctx.Repo.GitRepo, rel, attachmentUUIDs, newTagMsg); err != nil { handleTagReleaseError(err) return } - ctx.Redirect(ctx.Repo.RepoLink + "/releases") + ctx.JSONRedirect(ctx.Repo.RepoLink + "/releases") return } @@ -521,8 +512,7 @@ func NewReleasePost(ctx *context.Context) { // old logic: if the release is not a tag (it is a real release), do not update it on the "new release" page // add new logic: if tag-only, do not convert the tag to a release if form.TagOnly || !rel.IsTag { - ctx.Data["Err_TagName"] = true - ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) + ctx.JSONError(ctx.Tr("repo.release.tag_name_already_exist")) return } @@ -538,7 +528,7 @@ func NewReleasePost(ctx *context.Context) { handleTagReleaseError(err) return } - ctx.Redirect(ctx.Repo.RepoLink + "/releases") + ctx.JSONRedirect(ctx.Repo.RepoLink + "/releases") } // EditRelease render release edit page @@ -580,55 +570,39 @@ func EditRelease(ctx *context.Context) { return } ctx.Data["attachments"] = rel.Attachments - - // Get assignees. - assigneeUsers, err := repo_model.GetRepoAssignees(ctx, rel.Repo) - if err != nil { - ctx.ServerError("GetRepoAssignees", err) - return - } - ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers) - ctx.HTML(http.StatusOK, tplReleaseNew) } // EditReleasePost response for edit release func EditReleasePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditReleaseForm) - - newReleaseCommon(ctx) - if ctx.Written() { + if ctx.HasError() { + ctx.JSONError(ctx.GetErrMsg()) return } - ctx.Data["Title"] = ctx.Tr("repo.release.edit_release") - ctx.Data["PageIsEditRelease"] = true + form := web.GetForm(ctx).(*forms.EditReleaseForm) tagName := ctx.PathParam("*") rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tagName) if err != nil { if repo_model.IsErrReleaseNotExist(err) { - ctx.NotFound(err) + ctx.JSONErrorNotFound(err.Error()) } else { ctx.ServerError("GetRelease", err) } return } if rel.IsTag { - ctx.NotFound(err) // for a pure tag release, don't allow to edit it as a release + ctx.JSONErrorNotFound() // for a pure tag release, don't allow to edit it as a release return } + ctx.Data["tag_name"] = rel.TagName ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch) ctx.Data["title"] = rel.Title ctx.Data["content"] = rel.Note ctx.Data["prerelease"] = rel.IsPrerelease - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplReleaseNew) - return - } - const delPrefix = "attachment-del-" const editPrefix = "attachment-edit-" var addAttachmentUUIDs, delAttachmentUUIDs []string @@ -650,10 +624,14 @@ func EditReleasePost(ctx *context.Context) { rel.IsPrerelease = form.Prerelease if err = release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, addAttachmentUUIDs, delAttachmentUUIDs, editAttachments); err != nil { - ctx.ServerError("UpdateRelease", err) + if upload.IsErrFileTypeForbidden(err) { + ctx.JSONError(err.Error()) + } else { + ctx.ServerError("UpdateRelease", err) + } return } - ctx.Redirect(ctx.Repo.RepoLink + "/releases") + ctx.JSONRedirect(ctx.Repo.RepoLink + "/releases") } // DeleteRelease deletes a release diff --git a/routers/web/repo/release_test.go b/routers/web/repo/release_test.go index 82f3115ee5..57f831bffd 100644 --- a/routers/web/repo/release_test.go +++ b/routers/web/repo/release_test.go @@ -4,12 +4,14 @@ package repo import ( + "net/http/httptest" "testing" "gitea.dev/models/db" repo_model "gitea.dev/models/repo" "gitea.dev/models/unit" "gitea.dev/models/unittest" + "gitea.dev/modules/test" "gitea.dev/modules/web" "gitea.dev/services/context" "gitea.dev/services/contexttest" @@ -39,15 +41,15 @@ func TestNewReleasePost(t *testing.T) { assert.NotEmpty(t, ctx.Data["ShowCreateTagOnlyButton"]) }) - post := func(t *testing.T, form forms.NewReleaseForm) *context.Context { - ctx, _ := contexttest.MockContext(t, "user2/repo1/releases/new") + post := func(t *testing.T, form forms.NewReleaseForm) (*context.Context, *httptest.ResponseRecorder) { + ctx, resp := contexttest.MockContext(t, "user2/repo1/releases/new") contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) contexttest.LoadGitRepo(t, ctx) defer ctx.Repo.GitRepo.Close() web.SetForm(ctx, &form) NewReleasePost(ctx) - return ctx + return ctx, resp } loadRelease := func(t *testing.T, tagName string) *repo_model.Release { @@ -70,7 +72,7 @@ func TestNewReleasePost(t *testing.T) { }) t.Run("ReleaseExistsDoUpdate(non-tag)", func(t *testing.T) { - ctx := post(t, forms.NewReleaseForm{ + _, resp := post(t, forms.NewReleaseForm{ TagName: "v1.1", Target: "master", Title: "updated-title", @@ -80,11 +82,11 @@ func TestNewReleasePost(t *testing.T) { require.NotNil(t, rel) assert.False(t, rel.IsTag) assert.Equal(t, "testing-release", rel.Title) - assert.NotEmpty(t, ctx.Flash.ErrorMsg) + assert.NotEmpty(t, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage) }) t.Run("ReleaseExistsDoUpdate(tag-only)", func(t *testing.T) { - ctx := post(t, forms.NewReleaseForm{ + ctx, resp := post(t, forms.NewReleaseForm{ TagName: "delete-tag", // a strange name, but it is the only "is_tag=true" fixture Target: "master", Title: "updated-title", @@ -95,12 +97,12 @@ func TestNewReleasePost(t *testing.T) { require.NotNil(t, rel) assert.True(t, rel.IsTag) // the record should not be updated because the request is "tag-only". TODO: need to improve the logic? assert.Equal(t, "delete-tag", rel.Title) - assert.NotEmpty(t, ctx.Flash.ErrorMsg) + assert.NotEmpty(t, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage) assert.NotEmpty(t, ctx.Data["ShowCreateTagOnlyButton"]) // still show the "tag-only" button }) t.Run("ReleaseExistsDoUpdate(tag-release)", func(t *testing.T) { - ctx := post(t, forms.NewReleaseForm{ + ctx, _ := post(t, forms.NewReleaseForm{ TagName: "delete-tag", // a strange name, but it is the only "is_tag=true" fixture Target: "master", Title: "updated-title", @@ -114,7 +116,7 @@ func TestNewReleasePost(t *testing.T) { }) t.Run("TagOnly", func(t *testing.T) { - ctx := post(t, forms.NewReleaseForm{ + ctx, _ := post(t, forms.NewReleaseForm{ TagName: "new-tag-only", Target: "master", Title: "title", @@ -128,7 +130,7 @@ func TestNewReleasePost(t *testing.T) { }) t.Run("TagOnlyConflict", func(t *testing.T) { - ctx := post(t, forms.NewReleaseForm{ + _, resp := post(t, forms.NewReleaseForm{ TagName: "v1.1", Target: "master", Title: "title", @@ -138,7 +140,7 @@ func TestNewReleasePost(t *testing.T) { rel := loadRelease(t, "v1.1") require.NotNil(t, rel) assert.False(t, rel.IsTag) - assert.NotEmpty(t, ctx.Flash.ErrorMsg) + assert.NotEmpty(t, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage) }) } diff --git a/routers/web/repo/render.go b/routers/web/repo/render.go index b323da163c..748c08f53b 100644 --- a/routers/web/repo/render.go +++ b/routers/web/repo/render.go @@ -19,7 +19,7 @@ func RenderFile(ctx *context.Context) { var blob *git.Blob var err error if ctx.Repo.TreePath != "" { - blob, err = ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath) + blob, err = ctx.Repo.Commit.GetBlobByPath(ctx, ctx.Repo.GitRepo, ctx.Repo.TreePath) } else { blob, err = ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha")) } @@ -32,7 +32,7 @@ func RenderFile(ctx *context.Context) { return } - blobReader, err := blob.DataAsync() + blobReader, err := blob.DataAsync(ctx) if err != nil { ctx.ServerError("DataAsync", err) return diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index d23cca7fa5..ff213fcc42 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -351,7 +351,7 @@ func Download(ctx *context.Context) { return } - aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("*"), ctx.FormStrings("path")) + aReq, err := archiver_service.NewRequest(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("*"), ctx.FormStrings("path")) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.HTTPError(http.StatusBadRequest, err.Error()) @@ -387,7 +387,7 @@ func InitiateDownload(ctx *context.Context) { }) return } - aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("*"), paths) + aReq, err := archiver_service.NewRequest(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("*"), paths) if err != nil { ctx.HTTPError(http.StatusBadRequest, "invalid archive request") return diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go index 72e512e145..c8212e3b4c 100644 --- a/routers/web/repo/setting/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -122,7 +122,7 @@ func LFSLocks(ctx *context.Context) { return } - gitRepo, err := git.OpenRepository(ctx, tmpBasePath) + gitRepo, err := git.OpenRepositoryLocal(tmpBasePath) if err != nil { log.Error("Unable to open temporary repository: %s (%v)", tmpBasePath, err) ctx.ServerError("LFSLocks", fmt.Errorf("failed to open new temporary repository in: %s %w", tmpBasePath, err)) @@ -130,7 +130,7 @@ func LFSLocks(ctx *context.Context) { } defer gitRepo.Close() - checker, err := attribute.NewBatchChecker(gitRepo, ctx.Repo.Repository.DefaultBranch, []string{attribute.Lockable}) + checker, err := attribute.NewBatchChecker(ctx, gitRepo, ctx.Repo.Repository.DefaultBranch, []string{attribute.Lockable}) if err != nil { log.Error("Unable to check attributes in %s (%v)", tmpBasePath, err) ctx.ServerError("LFSLocks", err) @@ -151,7 +151,7 @@ func LFSLocks(ctx *context.Context) { } ctx.Data["Lockables"] = lockables - filelist, err := gitRepo.LsFiles(filenames...) + filelist, err := gitRepo.LsFiles(ctx, filenames...) if err != nil { log.Error("Unable to lsfiles in %s (%v)", tmpBasePath, err) ctx.ServerError("LFSLocks", err) @@ -383,7 +383,7 @@ func LFSFileFind(ctx *context.Context) { ctx.Data["Size"] = size ctx.Data["SHA"] = sha - results, err := pipeline.FindLFSFile(ctx.Repo.GitRepo, objectID) + results, err := pipeline.FindLFSFile(ctx, ctx.Repo.GitRepo, objectID) if err != nil && err != io.EOF { log.Error("Failure in FindLFSFile: %v", err) ctx.ServerError("LFSFind: FindLFSFile.", err) diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 26607fb40b..730a30a730 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -69,7 +69,7 @@ func SettingsCtxData(ctx *context.Context) { ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval ctx.Data["CanConvertFork"] = ctx.Repo.Repository.IsFork && ctx.Doer.CanCreateRepoIn(ctx.Repo.Repository.Owner) - signing, _ := gitrepo.GetSigningKey(ctx) + signing, _ := git.GetSigningKey(ctx) ctx.Data["SigningKeyAvailable"] = signing != nil ctx.Data["SigningSettings"] = setting.Repository.Signing ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled @@ -147,7 +147,7 @@ func SettingsPost(ctx *context.Context) { ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval - signing, _ := gitrepo.GetSigningKey(ctx) + signing, _ := git.GetSigningKey(ctx) ctx.Data["SigningKeyAvailable"] = signing != nil ctx.Data["SigningSettings"] = setting.Repository.Signing ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index c52f5fc4b1..f128209f1b 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -429,7 +429,7 @@ func telegramHookParams(ctx *context.Context) webhookParams { return webhookParams{ Type: webhook_module.TELEGRAM, - URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&message_thread_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID), url.QueryEscape(form.ThreadID)), + URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendRichMessage?chat_id=%s&message_thread_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID), url.QueryEscape(form.ThreadID)), ContentType: webhook.ContentTypeJSON, WebhookForm: form.WebhookForm, Meta: &webhook_service.TelegramMeta{ @@ -664,19 +664,14 @@ func TestWebhook(ctx *context.Context) { return } - // Grab latest commit or fake one if it's empty repository. - // Note: in old code, the "ctx.Repo.Commit" is the last commit of the default branch. - // New code doesn't set that commit, so it always uses the fake commit to test webhook. - commit := ctx.Repo.Commit - if commit == nil { - ghost := user_model.NewGhostUser() - objectFormat := git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName) - commit = &git.Commit{ - ID: objectFormat.EmptyObjectID(), - Author: ghost.NewGitSig(), - Committer: ghost.NewGitSig(), - CommitMessage: git.CommitMessage{MessageRaw: "This is a fake commit"}, - } + // use a fake commit to test webhook + ghostUser := user_model.NewGhostUser() + objectFormat := git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName) + commit := &git.Commit{ + ID: objectFormat.EmptyObjectID(), + Author: ghostUser.NewGitSig(), + Committer: ghostUser.NewGitSig(), + CommitMessage: git.CommitMessage{MessageRaw: "This is a fake commit for webhook push test"}, } apiUser := convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone) @@ -697,7 +692,7 @@ func TestWebhook(ctx *context.Context) { commitID := commit.ID.String() p := &api.PushPayload{ - Ref: git.BranchPrefix + ctx.Repo.Repository.DefaultBranch, + Ref: git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch).String(), Before: commitID, After: commitID, CompareURL: setting.AppURL + ctx.Repo.Repository.ComposeCompareURL(commitID, commitID), @@ -708,8 +703,8 @@ func TestWebhook(ctx *context.Context) { Pusher: apiUser, Sender: apiUser, } - if err := webhook_service.PrepareWebhook(ctx, w, webhook_module.HookEventPush, p); err != nil { - ctx.Flash.Error("PrepareWebhook: " + err.Error()) + if err := webhook_service.PrepareTestWebhook(ctx, w, webhook_module.HookEventPush, p); err != nil { + ctx.Flash.Error("PrepareTestWebhook: " + err.Error()) ctx.Status(http.StatusInternalServerError) } else { ctx.Flash.Info(ctx.Tr("repo.settings.webhook.delivery.success")) diff --git a/routers/web/repo/treelist.go b/routers/web/repo/treelist.go index 45889cbe5d..206660ad5c 100644 --- a/routers/web/repo/treelist.go +++ b/routers/web/repo/treelist.go @@ -22,13 +22,13 @@ import ( // TreeList get all files' entries of a repository func TreeList(ctx *context.Context) { - tree, err := ctx.Repo.Commit.SubTree("/") + tree, err := ctx.Repo.Commit.SubTree(ctx, ctx.Repo.GitRepo, "/") if err != nil { ctx.ServerError("Repo.Commit.SubTree", err) return } - entries, err := tree.ListEntriesRecursiveFast() + entries, err := tree.ListEntriesRecursiveFast(ctx, ctx.Repo.GitRepo) if err != nil { ctx.ServerError("ListEntriesRecursiveFast", err) return @@ -144,7 +144,7 @@ func transformDiffTreeForWeb(renderedIconPool *fileicon.RenderedIconPool, diffTr func TreeViewNodes(ctx *context.Context) { renderedIconPool := fileicon.NewRenderedIconPool() - results, err := files_service.GetTreeViewNodes(ctx, ctx.Repo.RepoLink, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path")) + results, err := files_service.GetTreeViewNodes(ctx, ctx.Repo.RepoLink, renderedIconPool, ctx.Repo.GitRepo, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path")) if err != nil { ctx.ServerError("GetTreeViewNodes", err) return diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 6df4e738c9..fecd55db6c 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -70,7 +70,7 @@ func (fi *fileInfo) isLFSFile() bool { } func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) (buf []byte, dataRc io.ReadCloser, fi *fileInfo, err error) { - dataRc, err = blob.DataAsync() + dataRc, err = blob.DataAsync(ctx) if err != nil { return nil, nil, nil, err } @@ -81,7 +81,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) (buf []b n, _ := util.ReadAtMost(dataRc, buf) buf = buf[:n] - fi = &fileInfo{blobOrLfsSize: blob.Size(), st: typesniffer.DetectContentType(buf)} + fi = &fileInfo{blobOrLfsSize: blob.Size(ctx), st: typesniffer.DetectContentType(buf)} // FIXME: what happens when README file is an image? if !fi.st.IsText() || !setting.LFS.StartServer { @@ -260,7 +260,7 @@ func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) { fileIcons := map[string]template.HTML{} for _, f := range files { fullPath := path.Join(ctx.Repo.TreePath, f.Entry.Name()) - entryInfo := fileicon.EntryInfoFromGitTreeEntry(ctx.Repo.Commit, fullPath, f.Entry) + entryInfo := fileicon.EntryInfoFromGitTreeEntry(ctx, ctx.Repo.GitRepo, ctx.Repo.Commit, fullPath, f.Entry) fileIcons[f.Entry.Name()] = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo) } fileIcons[".."] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder()) @@ -269,7 +269,7 @@ func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) { } func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entries { - tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath) + tree, err := ctx.Repo.Commit.SubTree(ctx, ctx.Repo.GitRepo, ctx.Repo.TreePath) if err != nil { HandleGitError(ctx, "Repo.Commit.SubTree", err) return nil @@ -280,7 +280,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri ctx.Data["LastCommitLoaderURL"] = lastCommitLoaderURL + "?refSubUrl=" + url.QueryEscape(ctx.Repo.RefTypeNameSubURL()) // Get current entry user currently looking at. - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx, ctx.Repo.GitRepo, ctx.Repo.TreePath) if err != nil { HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) return nil @@ -291,7 +291,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri return nil } - allEntries, err := tree.ListEntries() + allEntries, err := tree.ListEntries(ctx, ctx.Repo.GitRepo) if err != nil { ctx.ServerError("ListEntries", err) return nil @@ -305,7 +305,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri defer cancel() } - files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.RepoLink, ctx.Repo.Commit, ctx.Repo.TreePath) + files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.RepoLink, ctx.Repo.GitRepo, ctx.Repo.Commit, ctx.Repo.TreePath) if err != nil { ctx.ServerError("GetCommitsInfo", err) return nil @@ -328,6 +328,9 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri } ctx.Data["Files"] = files + ctx.Data["GetSubJumpablePathName"] = func(entry *git.TreeEntry) string { + return entry.GetSubJumpablePathName(ctx, ctx.Repo.GitRepo) + } prepareDirectoryFileIcons(ctx, files) for _, f := range files { if f.Commit == nil { diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index 6bbfc0cdeb..f9c43c5ca7 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -29,7 +29,7 @@ import ( ) func prepareLatestCommitInfo(ctx *context.Context) bool { - commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath) + commit, err := ctx.Repo.Commit.GetCommitByPath(ctx, ctx.Repo.GitRepo, ctx.Repo.TreePath) if err != nil { ctx.ServerError("GetCommitByPath", err) return false @@ -161,7 +161,7 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) { return } - blob := entry.Blob() + blob := entry.Blob(ctx.Repo.GitRepo) ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+ctx.Repo.TreePath, ctx.Repo.RefFullName.ShortName()) ctx.Data["FileIsSymlink"] = entry.IsLink() @@ -169,7 +169,7 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) if ctx.Repo.TreePath == ".editorconfig" { - _, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit) + _, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx, ctx.Repo.Commit) if editorconfigWarning != nil { ctx.Data["FileWarning"] = strings.TrimSpace(editorconfigWarning.Error()) } @@ -177,12 +177,12 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["FileError"] = strings.TrimSpace(editorconfigErr.Error()) } } else if issue_service.IsTemplateConfig(ctx.Repo.TreePath) { - _, issueConfigErr := issue_service.GetTemplateConfig(ctx.Repo.GitRepo, ctx.Repo.TreePath, ctx.Repo.Commit) + _, issueConfigErr := issue_service.GetTemplateConfig(ctx, ctx.Repo.GitRepo, ctx.Repo.TreePath, ctx.Repo.Commit) if issueConfigErr != nil { ctx.Data["FileError"] = strings.TrimSpace(issueConfigErr.Error()) } } else if actions.IsWorkflow(ctx.Repo.TreePath) { - content, err := actions.GetContentFromEntry(entry) + content, err := actions.GetContentFromEntry(ctx, ctx.Repo.GitRepo, entry) if err != nil { log.Error("actions.GetContentFromEntry: %v", err) } @@ -190,7 +190,7 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) } } else if issue_service.IsCodeOwnerFile(ctx.Repo.TreePath) { - if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil { + if data, err := blob.GetBlobContent(ctx, setting.UI.MaxDisplayFileSize); err == nil { _, warnings := issue_model.GetCodeOwnersFromContent(ctx, data) if len(warnings) > 0 { ctx.Data["FileWarning"] = strings.Join(warnings, "\n") diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index ae72ac1480..c696b3c9a1 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -11,6 +11,7 @@ import ( "strings" "time" + auth_model "gitea.dev/models/auth" "gitea.dev/models/db" git_model "gitea.dev/models/git" repo_model "gitea.dev/models/repo" @@ -36,7 +37,7 @@ func checkOutdatedBranch(ctx *context.Context) { } // get the head commit of the branch since ctx.Repo.CommitID is not always the head commit of `ctx.Repo.BranchName` - commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName) + commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx, ctx.Repo.BranchName) if err != nil { log.Error("GetBranchCommitID: %v", err) // Don't return an error page, as it can be rechecked the next time the user opens the page. @@ -66,7 +67,7 @@ func prepareHomeSidebarRepoTopics(ctx *context.Context) { ctx.Data["Topics"] = topics } -func prepareOpenWithEditorApps(ctx *context.Context) { +func prepareClonePanel(ctx *context.Context) { var tmplApps []map[string]any apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx) for _, app := range apps { @@ -92,6 +93,12 @@ func prepareOpenWithEditorApps(ctx *context.Context) { }) } ctx.Data["OpenWithEditorApps"] = tmplApps + + if !setting.Repository.DisableDownloadSourceArchives { + // FIXME: here it only uses the shortname in the ref to build the link, it can't distinguish the branch/tag/commit with the same name + // in the future, it's better to use something like "/archive/branch/the-name.zip", "/archive/tag/the-name.zip" */}} + ctx.Data["DownloadArchiveLinkPrefix"] = ctx.Repo.RepoLink + "/archive/" + util.PathEscapeSegments(ctx.Repo.RefFullName.ShortName()) + } } func prepareHomeSidebarCitationFile(entry *git.TreeEntry) func(ctx *context.Context) { @@ -99,12 +106,12 @@ func prepareHomeSidebarCitationFile(entry *git.TreeEntry) func(ctx *context.Cont if entry.Name() != "" { return } - tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath) + tree, err := ctx.Repo.Commit.SubTree(ctx, ctx.Repo.GitRepo, ctx.Repo.TreePath) if err != nil { HandleGitError(ctx, "Repo.Commit.SubTree", err) return } - allEntries, err := tree.ListEntries() + allEntries, err := tree.ListEntries(ctx, ctx.Repo.GitRepo) if err != nil { ctx.ServerError("ListEntries", err) return @@ -112,7 +119,7 @@ func prepareHomeSidebarCitationFile(entry *git.TreeEntry) func(ctx *context.Cont for _, entry := range allEntries { if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" { // Read Citation file contents - if content, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { + if content, err := entry.Blob(ctx.Repo.GitRepo).GetBlobContent(ctx, setting.UI.MaxDisplayFileSize); err != nil { log.Error("checkCitationFile: GetBlobContent: %v", err) } else { ctx.Data["CitiationExist"] = true @@ -219,7 +226,7 @@ func handleRepoEmptyOrBroken(ctx *context.Context) { ctx.Repo.GitRepo, _ = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) } if ctx.Repo.GitRepo != nil { - reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty() + reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty(ctx) if err != nil { showEmpty = true // the repo is broken updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryBroken) @@ -228,7 +235,7 @@ func handleRepoEmptyOrBroken(ctx *context.Context) { } else if reallyEmpty { showEmpty = true // the repo is really empty updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady) - } else if branches, _, _ := ctx.Repo.GitRepo.GetBranchNames(0, 1); len(branches) == 0 { + } else if branches, _, _ := ctx.Repo.GitRepo.GetBranchNames(ctx, 0, 1); len(branches) == 0 { showEmpty = true // it is not really empty, but there is no branch // at the moment, other repo units like "actions" are not able to handle such case, // so we just mark the repo as empty to prevent from displaying these units. @@ -289,7 +296,7 @@ func handleRepoViewSubmodule(ctx *context.Context, commitSubmoduleFile *git.Comm func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) { return func(ctx *context.Context) { if entry.IsSubModule() { - commitSubmoduleFile, err := git.GetCommitInfoSubmoduleFile(ctx.Repo.RepoLink, ctx.Repo.TreePath, ctx.Repo.Commit, entry.ID) + commitSubmoduleFile, err := git.GetCommitInfoSubmoduleFile(ctx, ctx.Repo.RepoLink, ctx.Repo.TreePath, ctx.Repo.GitRepo, ctx.Repo.Commit, entry.ID) if err != nil { HandleGitError(ctx, "prepareToRenderDirOrFile: GetCommitInfoSubmoduleFile", err) return @@ -350,7 +357,7 @@ func redirectFollowSymlink(ctx *context.Context, treePathEntry *git.TreeEntry) b return false } if treePathEntry.IsLink() { - if res, err := git.EntryFollowLinks(ctx.Repo.Commit, ctx.Repo.TreePath, treePathEntry); err == nil { + if res, err := git.EntryFollowLinks(ctx, ctx.Repo.GitRepo, ctx.Repo.Commit, ctx.Repo.TreePath, treePathEntry); err == nil { redirect := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(res.TargetFullPath) + "?" + ctx.Req.URL.RawQuery ctx.Redirect(redirect) return true @@ -394,6 +401,13 @@ func Home(ctx *context.Context) { return } + // a scoped or public-only API token authenticating this web request must still satisfy + // the repository read scope before private repo content is served + context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read) + if ctx.Written() { + return + } + // Check whether the repo is viewable: not in migration, and the code unit should be enabled // Ideally the "feed" logic should be after this, but old code did so, so keep it as-is. checkHomeCodeViewable(ctx) @@ -417,7 +431,7 @@ func Home(ctx *context.Context) { prepareHomeTreeSideBarSwitch(ctx) // get the current git entry which doer user is currently looking at. - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx, ctx.Repo.GitRepo, ctx.Repo.TreePath) if err != nil { HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) return @@ -431,7 +445,7 @@ func Home(ctx *context.Context) { isTreePathRoot := ctx.Repo.TreePath == "" prepareFuncs := []func(*context.Context){ - prepareOpenWithEditorApps, + prepareClonePanel, prepareHomeSidebarRepoTopics, checkOutdatedBranch, prepareToRenderDirOrFile(entry), diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index 2e50755d4f..908ff95dd1 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -66,9 +66,9 @@ func findReadmeFileInEntries(ctx *context.Context, parentDir string, entries []* for _, entry := range entries { if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok { fullPath := path.Join(parentDir, entry.Name()) - if readmeFiles[i] == nil || base.NaturalSortCompare(readmeFiles[i].Name(), entry.Blob().Name()) < 0 { + if readmeFiles[i] == nil || base.NaturalSortCompare(readmeFiles[i].Name(), entry.Blob(ctx.Repo.GitRepo).Name()) < 0 { if entry.IsLink() { - res, err := git.EntryFollowLinks(ctx.Repo.Commit, fullPath, entry) + res, err := git.EntryFollowLinks(ctx, ctx.Repo.GitRepo, ctx.Repo.Commit, fullPath, entry) if err == nil && (res.TargetEntry.IsExecutable() || res.TargetEntry.IsRegular()) { readmeFiles[i] = entry } @@ -92,12 +92,12 @@ func findReadmeFileInEntries(ctx *context.Context, parentDir string, entries []* if subTreeEntry == nil { continue } - subTree := subTreeEntry.Tree() + subTree := subTreeEntry.Tree(ctx, ctx.Repo.GitRepo) if subTree == nil { // this should be impossible; if subTreeEntry exists so should this. continue } - childEntries, err := subTree.ListEntries() + childEntries, err := subTree.ListEntries(ctx, ctx.Repo.GitRepo) if err != nil { return "", nil, err } @@ -145,7 +145,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil readmeFullPath := path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name()) readmeTargetEntry := readmeFile if readmeFile.IsLink() { - if res, err := git.EntryFollowLinks(ctx.Repo.Commit, readmeFullPath, readmeFile); err == nil { + if res, err := git.EntryFollowLinks(ctx, ctx.Repo.GitRepo, ctx.Repo.Commit, readmeFullPath, readmeFile); err == nil { readmeTargetEntry = res.TargetEntry } else { readmeTargetEntry = nil // if we cannot resolve the symlink, we cannot render the readme, ignore the error @@ -160,7 +160,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil ctx.Data["ReadmeExist"] = true ctx.Data["FileIsSymlink"] = readmeFile.IsLink() - buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, readmeTargetEntry.Blob()) + buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, readmeTargetEntry.Blob(ctx.Repo.GitRepo)) if err != nil { ctx.ServerError("getFileReader", err) return diff --git a/routers/web/repo/view_readme_test.go b/routers/web/repo/view_readme_test.go index 43bee26450..851b7c88ca 100644 --- a/routers/web/repo/view_readme_test.go +++ b/routers/web/repo/view_readme_test.go @@ -41,18 +41,19 @@ data 12 err = gitcmd.NewCommand("fast-import").WithDir(repoPath).WithStdinBytes([]byte(stdin)).RunWithStderr(t.Context()) require.NoError(t, err) - gitRepo, err := git.OpenRepository(t.Context(), repoPath) + gitRepo, err := git.OpenRepositoryLocal(repoPath) require.NoError(t, err) defer gitRepo.Close() - commit, err := gitRepo.GetBranchCommit("master") + commit, err := gitRepo.GetBranchCommit(t.Context(), "master") require.NoError(t, err) - entries, err := commit.ListEntries() + entries, err := commit.Tree().ListEntries(t.Context(), gitRepo) require.NoError(t, err) ctx, _ := contexttest.MockContext(t, "/") ctx.Repo.Commit = commit + ctx.Repo.GitRepo = gitRepo foundDir, foundReadme, err := findReadmeFileInEntries(ctx, "", entries, true) require.NoError(t, err) require.NotNil(t, foundReadme) @@ -62,7 +63,7 @@ data 12 assert.True(t, foundReadme.IsLink()) // Verify that it can follow the link - res, err := git.EntryFollowLinks(commit, path.Join(foundDir, foundReadme.Name()), foundReadme) + res, err := git.EntryFollowLinks(t.Context(), gitRepo, commit, path.Join(foundDir, foundReadme.Name()), foundReadme) require.NoError(t, err) assert.Equal(t, "target.md", res.TargetFullPath) }) diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 6e2133a945..75de035164 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -6,6 +6,7 @@ package repo import ( "bytes" + gocontext "context" "html/template" "io" "net/http" @@ -78,8 +79,8 @@ type PageMeta struct { } // findEntryForFile finds the tree entry for a target filepath. -func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) { - entry, err := commit.GetTreeEntryByPath(target) +func findEntryForFile(ctx gocontext.Context, wikiRepo *git.Repository, commit *git.Commit, target string) (*git.TreeEntry, error) { + entry, err := commit.GetTreeEntryByPath(ctx, wikiRepo, target) if err != nil && !git.IsErrNotExist(err) { return nil, err } @@ -92,7 +93,7 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) if unescapedTarget, err = url.QueryUnescape(target); err != nil { return nil, err } - return commit.GetTreeEntryByPath(unescapedTarget) + return commit.GetTreeEntryByPath(ctx, wikiRepo, unescapedTarget) } func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) { @@ -102,7 +103,7 @@ func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, err return nil, nil, errGitRepo } - commit, errCommit := wikiGitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch) + commit, errCommit := wikiGitRepo.GetBranchCommit(ctx, ctx.Repo.Repository.DefaultWikiBranch) if git.IsErrNotExist(errCommit) { // if the default branch recorded in database is out of sync, then re-sync it gitRepoDefaultBranch, errBranch := gitrepo.GetDefaultBranch(ctx, ctx.Repo.Repository.WikiStorageRepo()) @@ -116,7 +117,7 @@ func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, err } ctx.Repo.Repository.DefaultWikiBranch = gitRepoDefaultBranch // retry to get the commit from the correct default branch - commit, errCommit = wikiGitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch) + commit, errCommit = wikiGitRepo.GetBranchCommit(ctx, ctx.Repo.Repository.DefaultWikiBranch) } if errCommit != nil { return wikiGitRepo, nil, errCommit @@ -126,8 +127,8 @@ func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, err // wikiContentsByEntry returns the contents of the wiki page referenced by the // given tree entry. Writes to ctx if an error occurs. -func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte { - reader, err := entry.Blob().DataAsync() +func wikiContentsByEntry(ctx *context.Context, wikiRepo *git.Repository, entry *git.TreeEntry) []byte { + reader, err := entry.Blob(wikiRepo).DataAsync(ctx) if err != nil { ctx.ServerError("Blob.Data", err) return nil @@ -144,10 +145,10 @@ func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte { // wikiEntryByName returns the entry of a wiki page, along with a boolean // indicating whether the entry exists. Writes to ctx if an error occurs. // The last return value indicates whether the file should be returned as a raw file -func wikiEntryByName(ctx *context.Context, commit *git.Commit, wikiName wiki_service.WebPath) (*git.TreeEntry, string, bool, bool) { +func wikiEntryByName(ctx *context.Context, wikiRepo *git.Repository, commit *git.Commit, wikiName wiki_service.WebPath) (*git.TreeEntry, string, bool, bool) { isRaw := false gitFilename := wiki_service.WebPathToGitPath(wikiName) - entry, err := findEntryForFile(commit, gitFilename) + entry, err := findEntryForFile(ctx, wikiRepo, commit, gitFilename) if err != nil && !git.IsErrNotExist(err) { ctx.ServerError("findEntryForFile", err) return nil, "", false, false @@ -155,7 +156,7 @@ func wikiEntryByName(ctx *context.Context, commit *git.Commit, wikiName wiki_ser if entry == nil { // check if the file without ".md" suffix exists gitFilename := strings.TrimSuffix(gitFilename, ".md") - entry, err = findEntryForFile(commit, gitFilename) + entry, err = findEntryForFile(ctx, wikiRepo, commit, gitFilename) if err != nil && !git.IsErrNotExist(err) { ctx.ServerError("findEntryForFile", err) return nil, "", false, false @@ -170,12 +171,12 @@ func wikiEntryByName(ctx *context.Context, commit *git.Commit, wikiName wiki_ser // wikiContentsByName returns the contents of a wiki page, along with a boolean // indicating whether the page exists. Writes to ctx if an error occurs. -func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName wiki_service.WebPath) ([]byte, *git.TreeEntry, string, bool) { - entry, gitFilename, noEntry, _ := wikiEntryByName(ctx, commit, wikiName) +func wikiContentsByName(ctx *context.Context, wikiRepo *git.Repository, commit *git.Commit, wikiName wiki_service.WebPath) ([]byte, *git.TreeEntry, string, bool) { + entry, gitFilename, noEntry, _ := wikiEntryByName(ctx, wikiRepo, commit, wikiName) if entry == nil { return nil, nil, "", true } - return wikiContentsByEntry(ctx, entry), entry, gitFilename, noEntry + return wikiContentsByEntry(ctx, wikiRepo, entry), entry, gitFilename, noEntry } func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { @@ -188,7 +189,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { } // get the wiki pages list. - entries, err := commit.ListEntries() + entries, err := commit.Tree().ListEntries(ctx, wikiGitRepo) if err != nil { ctx.ServerError("ListEntries", err) return nil, nil @@ -233,7 +234,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { isFooter := pageName == "_Footer" // lookup filename in wiki - get gitTree entry , real filename - entry, pageFilename, noEntry, isRaw := wikiEntryByName(ctx, commit, pageName) + entry, pageFilename, noEntry, isRaw := wikiEntryByName(ctx, wikiGitRepo, commit, pageName) if noEntry { ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages") } @@ -245,7 +246,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { } // get page content - data := wikiContentsByEntry(ctx, entry) + data := wikiContentsByEntry(ctx, wikiGitRepo, entry) if ctx.Written() { return nil, nil } @@ -283,7 +284,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { } if !isSideBar { - sidebarContent, _, _, _ := wikiContentsByName(ctx, commit, "_Sidebar") + sidebarContent, _, _, _ := wikiContentsByName(ctx, wikiGitRepo, commit, "_Sidebar") if ctx.Written() { return nil, nil } @@ -295,7 +296,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { } if !isFooter { - footerContent, _, _, _ := wikiContentsByName(ctx, commit, "_Footer") + footerContent, _, _, _ := wikiContentsByName(ctx, wikiGitRepo, commit, "_Footer") if ctx.Written() { return nil, nil } @@ -335,7 +336,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.Data["title"] = displayName // lookup filename in wiki - get page content, gitTree entry , real filename - _, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName) + _, entry, pageFilename, noEntry := wikiContentsByName(ctx, wikiGitRepo, commit, pageName) if noEntry { ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages") } @@ -351,7 +352,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) page := max(ctx.FormInt("page"), 1) // get Commit Count - commitsHistory, _, err := wikiGitRepo.CommitsByFileAndRange( + commitsHistory, _, err := wikiGitRepo.CommitsByFileAndRange(ctx, git.CommitsByFileAndRangeOptions{ Revision: ctx.Repo.Repository.DefaultWikiBranch, File: pageFilename, @@ -375,7 +376,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) } func renderEditPage(ctx *context.Context) { - _, commit, err := findWikiRepoCommit(ctx) + wikiGitRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { if !git.IsErrNotExist(err) { ctx.ServerError("GetBranchCommit", err) @@ -396,7 +397,7 @@ func renderEditPage(ctx *context.Context) { ctx.Data["title"] = displayName // lookup filename in wiki - gitTree entry , real filename - entry, _, noEntry, isRaw := wikiEntryByName(ctx, commit, pageName) + entry, _, noEntry, isRaw := wikiEntryByName(ctx, wikiGitRepo, commit, pageName) if noEntry { ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages") } @@ -408,7 +409,7 @@ func renderEditPage(ctx *context.Context) { } // get wiki page content - data := wikiContentsByEntry(ctx, entry) + data := wikiContentsByEntry(ctx, wikiGitRepo, entry) if ctx.Written() { return } @@ -491,7 +492,7 @@ func Wiki(ctx *context.Context) { ctx.Data["FormatWarning"] = "File extension " + path.Ext(wikiPath) + " is not supported at the moment. Rendered as Markdown." } // Get last change information. - lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath) + lastCommit, err := wikiGitRepo.GetCommitByPath(ctx, wikiPath) if err != nil { ctx.ServerError("GetCommitByPath", err) return @@ -523,7 +524,7 @@ func WikiRevision(ctx *context.Context) { // Get last change information. wikiPath := entry.Name() - lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath) + lastCommit, err := wikiGitRepo.GetCommitByPath(ctx, wikiPath) if err != nil { ctx.ServerError("GetCommitByPath", err) return @@ -543,27 +544,27 @@ func WikiPages(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.wiki.pages") ctx.Data["CanWriteWiki"] = ctx.Repo.Permission.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived - _, commit, err := findWikiRepoCommit(ctx) + wikiGitRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { ctx.Redirect(ctx.Repo.RepoLink + "/wiki") return } treePath := "" // To support list sub folders' pages in the future - tree, err := commit.SubTree(treePath) + tree, err := commit.SubTree(ctx, wikiGitRepo, treePath) if err != nil { ctx.ServerError("SubTree", err) return } - allEntries, err := tree.ListEntries() + allEntries, err := tree.ListEntries(ctx, wikiGitRepo) if err != nil { ctx.ServerError("ListEntries", err) return } allEntries.CustomSort(base.NaturalSortCompare) - entries, _, err := allEntries.GetCommitsInfo(ctx, ctx.Repo.RepoLink, commit, treePath) + entries, _, err := allEntries.GetCommitsInfo(ctx, ctx.Repo.RepoLink, wikiGitRepo, commit, treePath) if err != nil { ctx.ServerError("GetCommitsInfo", err) return @@ -597,7 +598,7 @@ func WikiPages(ctx *context.Context) { // WikiRaw outputs raw blob requested by user (image for example) func WikiRaw(ctx *context.Context) { - _, commit, err := findWikiRepoCommit(ctx) + wikiGitRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { if git.IsErrNotExist(err) { ctx.NotFound(nil) @@ -612,7 +613,7 @@ func WikiRaw(ctx *context.Context) { var entry *git.TreeEntry if commit != nil { // Try to find a file with that name - entry, err = findEntryForFile(commit, providedGitPath) + entry, err = findEntryForFile(ctx, wikiGitRepo, commit, providedGitPath) if err != nil && !git.IsErrNotExist(err) { ctx.ServerError("findFile", err) return @@ -621,7 +622,7 @@ func WikiRaw(ctx *context.Context) { if entry == nil { // Try to find a wiki page with that name providedGitPath = strings.TrimSuffix(providedGitPath, ".md") - entry, err = findEntryForFile(commit, providedGitPath) + entry, err = findEntryForFile(ctx, wikiGitRepo, commit, providedGitPath) if err != nil && !git.IsErrNotExist(err) { ctx.ServerError("findFile", err) return @@ -630,7 +631,7 @@ func WikiRaw(ctx *context.Context) { } if entry != nil { - if err = common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, entry.Blob(), nil); err != nil { + if err = common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, entry.Blob(wikiGitRepo), nil); err != nil { ctx.ServerError("ServeBlob", err) } return diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index 28702361e5..d90d2f232d 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -28,28 +28,30 @@ const ( message = "Wiki commit message for unit tests" ) -func wikiEntry(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) *git.TreeEntry { - wikiRepo, err := gitrepo.OpenRepository(t.Context(), repo.WikiStorageRepo()) +func wikiEntry(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) (*git.Repository, *git.TreeEntry) { + wikiRepo, err := gitrepo.OpenRepository(repo.WikiStorageRepo()) assert.NoError(t, err) - defer wikiRepo.Close() - commit, err := wikiRepo.GetBranchCommit("master") + t.Cleanup(func() { + defer wikiRepo.Close() + }) + commit, err := wikiRepo.GetBranchCommit(t.Context(), "master") assert.NoError(t, err) - entries, err := commit.ListEntries() + entries, err := commit.Tree().ListEntries(t.Context(), wikiRepo) assert.NoError(t, err) for _, entry := range entries { if entry.Name() == wiki_service.WebPathToGitPath(wikiName) { - return entry + return wikiRepo, entry } } - return nil + return wikiRepo, nil } func wikiContent(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) string { - entry := wikiEntry(t, repo, wikiName) + wikiRepo, entry := wikiEntry(t, repo, wikiName) if !assert.NotNil(t, entry) { return "" } - reader, err := entry.Blob().DataAsync() + reader, err := entry.Blob(wikiRepo).DataAsync(t.Context()) assert.NoError(t, err) defer reader.Close() bytes, err := io.ReadAll(reader) @@ -58,11 +60,13 @@ func wikiContent(t *testing.T, repo *repo_model.Repository, wikiName wiki_servic } func assertWikiExists(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) { - assert.NotNil(t, wikiEntry(t, repo, wikiName)) + _, entry := wikiEntry(t, repo, wikiName) + assert.NotNil(t, entry) } func assertWikiNotExists(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) { - assert.Nil(t, wikiEntry(t, repo, wikiName)) + _, entry := wikiEntry(t, repo, wikiName) + assert.Nil(t, entry) } func assertPagesMetas(t *testing.T, expectedNames []string, metas any) { diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index f174bfb1cd..21a2bb8fc1 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -373,16 +373,18 @@ func RunnerBulkActionPost(ctx *context.Context) { return } - var runnerIDs []int64 - if rCtx.IsAdmin { - // ATTENTION: it completely depends on the assumption that the doer is "site admin" - // So it doesn't do extra permission check to the runner IDs - // In the future, if you need to support such operation on non-admin pages, be careful! - runnerIDs = ctx.FormStringInt64s("ids") - } else { + if !rCtx.IsAdmin { ctx.HTTPError(http.StatusForbidden, "bulk actions are admin-only") return } + // ATTENTION: it completely depends on the assumption that the doer is "site admin" + // So it doesn't do extra permission check to the runner IDs + // In the future, if you need to support such operation on non-admin pages, be careful! + runnerIDs := ctx.FormStringInt64s("ids") + if len(runnerIDs) == 0 { + ctx.HTTPError(http.StatusBadRequest, "missing runner IDs") + return + } action := ctx.FormString("action") var successKey, failedKey string diff --git a/routers/web/shared/actions/scoped_workflows.go b/routers/web/shared/actions/scoped_workflows.go new file mode 100644 index 0000000000..14d4efa7c6 --- /dev/null +++ b/routers/web/shared/actions/scoped_workflows.go @@ -0,0 +1,368 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "errors" + "net/http" + "slices" + "strings" + + actions_model "gitea.dev/models/actions" + repo_model "gitea.dev/models/repo" + actions_module "gitea.dev/modules/actions" + "gitea.dev/modules/actions/jobparser" + "gitea.dev/modules/container" + "gitea.dev/modules/log" + "gitea.dev/modules/setting" + "gitea.dev/modules/templates" + "gitea.dev/modules/util" + shared_user "gitea.dev/routers/web/shared/user" + actions_service "gitea.dev/services/actions" + "gitea.dev/services/context" +) + +const ( + tplOrgScopedWorkflows templates.TplName = "org/settings/actions" + tplUserScopedWorkflows templates.TplName = "user/settings/actions" + tplAdminScopedWorkflows templates.TplName = "admin/actions" +) + +type scopedWorkflowsCtx struct { + OwnerID int64 // 0 = instance-level + IsOrg bool + IsUser bool + IsGlobal bool + Template templates.TplName + RedirectLink string + // SearchUID is the uid passed to the repo-search box. For org/user it scopes the search to that owner; + // for admin (0) it searches all repos and therefore requires admin access on the route. + SearchUID int64 +} + +func getScopedWorkflowsCtx(ctx *context.Context) (*scopedWorkflowsCtx, error) { + if ctx.Data["PageIsOrgSettings"] == true { + if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { + ctx.ServerError("RenderUserOrgHeader", err) + return nil, nil //nolint:nilnil // error is already handled by ctx.ServerError + } + return &scopedWorkflowsCtx{ + OwnerID: ctx.Org.Organization.ID, + IsOrg: true, + Template: tplOrgScopedWorkflows, + RedirectLink: ctx.Org.OrgLink + "/settings/actions/scoped-workflows", + SearchUID: ctx.Org.Organization.ID, + }, nil + } + + if ctx.Data["PageIsUserSettings"] == true { + return &scopedWorkflowsCtx{ + OwnerID: ctx.Doer.ID, + IsUser: true, + Template: tplUserScopedWorkflows, + RedirectLink: setting.AppSubURL + "/user/settings/actions/scoped-workflows", + SearchUID: ctx.Doer.ID, + }, nil + } + + if ctx.Data["PageIsAdmin"] == true { + return &scopedWorkflowsCtx{ + OwnerID: 0, + IsGlobal: true, + Template: tplAdminScopedWorkflows, + RedirectLink: setting.AppSubURL + "/-/admin/actions/scoped-workflows", + SearchUID: 0, + }, nil + } + + return nil, errors.New("unable to set scoped workflows context") +} + +// scopedWorkflowInfo is one scoped workflow shown on the settings page, merged with its stored merge-gate config. +type scopedWorkflowInfo struct { + EntryName string + DisplayName string + Required bool + Patterns string // newline-joined stored status-check patterns (kept even when not required, as history) + Contexts []string // the commit-status contexts this workflow is expected to post, to preview which patterns match + Missing bool // the workflow file no longer exists on the source default branch, but a stored config lingers and must stay clearable +} + +// scopedWorkflowSourceView is the per-source data shown on the settings page. +type scopedWorkflowSourceView struct { + Repo *repo_model.Repository + ScopedWorkflowInfos []scopedWorkflowInfo +} + +func ScopedWorkflows(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.scoped_workflows") + ctx.Data["PageType"] = "scoped-workflows" + ctx.Data["PageIsSharedSettingsScopedWorkflows"] = true + + swCtx, err := getScopedWorkflowsCtx(ctx) + if err != nil { + ctx.ServerError("getScopedWorkflowsCtx", err) + return + } + if ctx.Written() { + return + } + + switch { + case swCtx.IsOrg: + ctx.Data["ScopedWorkflowsDesc"] = ctx.Tr("actions.scoped_workflows.desc_org") + case swCtx.IsUser: + ctx.Data["ScopedWorkflowsDesc"] = ctx.Tr("actions.scoped_workflows.desc_user") + default: // instance-level + ctx.Data["ScopedWorkflowsDesc"] = ctx.Tr("actions.scoped_workflows.desc_global") + } + + sources, err := actions_model.GetScopedWorkflowSourcesByOwner(ctx, swCtx.OwnerID) + if err != nil { + ctx.ServerError("GetScopedWorkflowSourcesByOwner", err) + return + } + + views := make([]*scopedWorkflowSourceView, 0, len(sources)) + for _, src := range sources { + repo, err := repo_model.GetRepositoryByID(ctx, src.SourceRepoID) + if err != nil { + log.Error("scoped workflows settings: load source repo %d: %v", src.SourceRepoID, err) + continue + } + views = append(views, &scopedWorkflowSourceView{ + Repo: repo, + ScopedWorkflowInfos: listSourceScopedWorkflowFiles(ctx, repo, src.WorkflowConfigs), + }) + } + + ctx.Data["ScopedWorkflowSources"] = views + ctx.Data["RepoSearchUID"] = swCtx.SearchUID + // owner/user scopes the repo search to the owner (exclusive); + // instance-level (admin) searches all repos and so must submit owner/name to disambiguate the selection across owners. + ctx.Data["ScopedWorkflowsSearchExclusive"] = !swCtx.IsGlobal + ctx.Data["ScopedWorkflowsSearchFullName"] = swCtx.IsGlobal + ctx.Data["RedirectLink"] = swCtx.RedirectLink + ctx.Data["ScopedWorkflowDirs"] = strings.Join(setting.Actions.ScopedWorkflowDirs, ", ") + ctx.HTML(http.StatusOK, swCtx.Template) +} + +// parsePatternLines splits a textarea value into trimmed, non-empty status-check patterns (one per line). +func parsePatternLines(raw string) []string { + var patterns []string + for line := range strings.SplitSeq(raw, "\n") { + if p := strings.TrimSpace(line); p != "" { + patterns = append(patterns, p) + } + } + return patterns +} + +// deriveScopedStatusContexts returns the commit-status contexts a scoped workflow is expected to post on a consumer: +// ": / ()" for each parsed job (matrix-expanded, matching run creation) and triggering event. +// Job names that depend on run-context expressions cannot resolve here (no run context) and appear as authored; a glob pattern still matches them. +func deriveScopedStatusContexts(prefix, displayName string, content []byte, events []*jobparser.Event) []string { + parsed, err := jobparser.Parse(content) + if err != nil { + return nil + } + eventNames := make([]string, 0, len(events)) + for _, e := range events { + // only events whose runs post a commit status can be a required check; workflow_dispatch, schedule, etc. post none. + if actions_module.ShouldEventCreateCommitStatus(e.Name) { + eventNames = append(eventNames, e.Name) + } + } + seen := make(container.Set[string]) + contexts := make([]string, 0, len(parsed)*len(eventNames)) + for _, sw := range parsed { + _, job := sw.Job() + if job == nil { + continue + } + jobName := util.EllipsisDisplayString(job.Name, 255) // run creation truncates job names the same way + for _, ev := range eventNames { + ctxName := actions_module.ScopedWorkflowStatusContextName(prefix, displayName, jobName, ev) + if seen.Contains(ctxName) { + continue + } + seen.Add(ctxName) + contexts = append(contexts, ctxName) + } + } + return contexts +} + +func listSourceScopedWorkflowFiles(ctx *context.Context, repo *repo_model.Repository, configs map[string]*actions_model.ScopedWorkflowConfig) []scopedWorkflowInfo { + rendered := make(container.Set[string], len(configs)) + files := make([]scopedWorkflowInfo, 0, len(configs)) + + // An empty source repo (or one that fails to parse) has no live workflow files, but a previously-saved config may still linger; + // fall through to surface those as orphan rows below so they remain clearable. + if !repo.IsEmpty { + _, parsed, err := actions_service.LoadParsedScopedWorkflows(ctx, repo) + if err != nil { + log.Error("scoped workflows settings: parse %s: %v", repo.FullName(), err) + } else { + for _, p := range parsed { + info := scopedWorkflowInfo{ + EntryName: p.EntryName, + DisplayName: p.DisplayName, + Contexts: deriveScopedStatusContexts(repo.FullName(), p.DisplayName, p.Content, p.Events), + } + if cfg := configs[p.EntryName]; cfg != nil { + info.Required = cfg.Required + info.Patterns = strings.Join(cfg.Patterns, "\n") + } + rendered.Add(p.EntryName) + files = append(files, info) + } + } + } + + // Surface configs whose workflow file no longer exists on the source default branch as orphan rows. + // A required orphan still gates merges (must-present), so the owner/admin must be able to see and clear it; + // otherwise the only escape would be removing the whole source registration. + orphans := make([]scopedWorkflowInfo, 0, len(configs)) + for name, cfg := range configs { + if cfg == nil || rendered.Contains(name) { + continue + } + orphans = append(orphans, scopedWorkflowInfo{ + EntryName: name, + DisplayName: name, + Required: cfg.Required, + Patterns: strings.Join(cfg.Patterns, "\n"), + Missing: true, + }) + } + // map iteration order is random; sort orphans for a stable settings page + slices.SortFunc(orphans, func(a, b scopedWorkflowInfo) int { return strings.Compare(a.EntryName, b.EntryName) }) + return append(files, orphans...) +} + +func ScopedWorkflowAdd(ctx *context.Context) { + swCtx, err := getScopedWorkflowsCtx(ctx) + if err != nil { + ctx.ServerError("getScopedWorkflowsCtx", err) + return + } + if ctx.Written() { + return + } + + repoName := ctx.FormString("repo_name") + var repo *repo_model.Repository + if swCtx.IsGlobal { + // instance-level: the source may be any repo on the instance, identified by owner/name + ownerName, name, ok := strings.Cut(repoName, "/") + if !ok { + ctx.JSONError(ctx.Tr("actions.scoped_workflows.source.not_found")) + return + } + repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, name) + } else { + // owner-level: resolve within the owner, which also enforces that the source is one of the owner's own repositories + repo, err = repo_model.GetRepositoryByName(ctx, swCtx.OwnerID, repoName) + } + if err != nil { + ctx.JSONError(ctx.Tr("actions.scoped_workflows.source.not_found")) + return + } + + if err := actions_model.AddScopedWorkflowSource(ctx, swCtx.OwnerID, repo.ID); err != nil { + ctx.ServerError("AddScopedWorkflowSource", err) + return + } + ctx.Flash.Success(ctx.Tr("actions.scoped_workflows.source.add_success")) + ctx.JSONRedirect(swCtx.RedirectLink) +} + +func ScopedWorkflowSetRequired(ctx *context.Context) { + swCtx, err := getScopedWorkflowsCtx(ctx) + if err != nil { + ctx.ServerError("getScopedWorkflowsCtx", err) + return + } + if ctx.Written() { + return + } + + repoID := ctx.FormInt64("repo_id") + + // the source must be registered for this owner + if _, err := actions_model.GetScopedWorkflowSource(ctx, swCtx.OwnerID, repoID); err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.JSONError(ctx.Tr("actions.scoped_workflows.source.not_found")) + } else { + ctx.ServerError("GetScopedWorkflowSource", err) + } + return + } + + // Live workflow entry names on the source default branch, used to distinguish orphan configs (whose workflow file no longer exists) from live ones. + sourceRepo, err := repo_model.GetRepositoryByID(ctx, repoID) + if err != nil { + ctx.ServerError("GetRepositoryByID", err) + return + } + liveSet := make(container.Set[string]) + if !sourceRepo.IsEmpty { // an empty source has no live workflows + _, parsed, err := actions_service.LoadParsedScopedWorkflows(ctx, sourceRepo) + if err != nil { + ctx.ServerError("LoadParsedScopedWorkflows", err) + return + } + for _, p := range parsed { + liveSet.Add(p.EntryName) + } + } + + // Every workflow row submits its ID in workflow_ids and its patterns (one per line) in required_patterns[]; + // checked rows additionally submit their ID in required_workflow_ids. + // A required workflow must have at least one pattern. + requiredSet := make(container.Set[string]) + for _, workflowID := range ctx.FormStrings("required_workflow_ids") { + requiredSet.Add(workflowID) + } + configs := make(map[string]*actions_model.ScopedWorkflowConfig) + for _, workflowID := range ctx.FormStrings("workflow_ids") { + patterns := parsePatternLines(ctx.FormString("required_patterns[" + workflowID + "]")) + required := requiredSet.Contains(workflowID) + if required && len(patterns) == 0 { + ctx.JSONError(ctx.Tr("actions.scoped_workflows.required.patterns_empty")) + return + } + // Keep a config only if it is required, or it is a still-existing. + // An orphan (file no longer in the source) that is not required is dropped. + if required || (liveSet.Contains(workflowID) && len(patterns) > 0) { + configs[workflowID] = &actions_model.ScopedWorkflowConfig{Required: required, Patterns: patterns} + } + } + if err := actions_model.SetScopedWorkflowSourceConfigs(ctx, swCtx.OwnerID, repoID, configs); err != nil { + ctx.ServerError("SetScopedWorkflowSourceConfigs", err) + return + } + ctx.Flash.Success(ctx.Tr("actions.scoped_workflows.required.update_success")) + ctx.JSONRedirect(swCtx.RedirectLink) +} + +func ScopedWorkflowRemove(ctx *context.Context) { + swCtx, err := getScopedWorkflowsCtx(ctx) + if err != nil { + ctx.ServerError("getScopedWorkflowsCtx", err) + return + } + if ctx.Written() { + return + } + + repoID := ctx.FormInt64("repo_id") + if err := actions_model.RemoveScopedWorkflowSource(ctx, swCtx.OwnerID, repoID); err != nil { + ctx.ServerError("RemoveScopedWorkflowSource", err) + return + } + ctx.Flash.Success(ctx.Tr("actions.scoped_workflows.source.remove_success")) + ctx.JSONRedirect(swCtx.RedirectLink) +} diff --git a/routers/web/shared/actions/scoped_workflows_test.go b/routers/web/shared/actions/scoped_workflows_test.go new file mode 100644 index 0000000000..7d96c431ee --- /dev/null +++ b/routers/web/shared/actions/scoped_workflows_test.go @@ -0,0 +1,75 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + actions_module "gitea.dev/modules/actions" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDeriveScopedStatusContexts(t *testing.T) { + t.Run("jobs x events; job name is its name: or its id", func(t *testing.T) { + content := []byte(`name: CI +on: [push, pull_request] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - run: echo + build: + name: Build It + runs-on: ubuntu-latest + steps: + - run: echo +`) + events, err := actions_module.GetEventsFromContent(content) + require.NoError(t, err) + got := deriveScopedStatusContexts("org/src", "CI", content, events) + assert.ElementsMatch(t, []string{ + "org/src: CI / lint (push)", + "org/src: CI / lint (pull_request)", + "org/src: CI / Build It (push)", + "org/src: CI / Build It (pull_request)", + }, got) + }) + + t.Run("only status-producing events; workflow_dispatch/schedule/workflow_call skipped", func(t *testing.T) { + content := []byte(`name: CI +on: + push: + workflow_dispatch: + workflow_call: + schedule: + - cron: "0 0 * * *" +jobs: + j: + runs-on: ubuntu-latest + steps: + - run: echo +`) + events, err := actions_module.GetEventsFromContent(content) + require.NoError(t, err) + got := deriveScopedStatusContexts("org/src", "CI", content, events) + assert.Equal(t, []string{"org/src: CI / j (push)"}, got) // only push posts a commit status + }) + + t.Run("a workflow_dispatch-only workflow has no expected contexts", func(t *testing.T) { + content := []byte(`name: Deploy +on: workflow_dispatch +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - run: echo +`) + events, err := actions_module.GetEventsFromContent(content) + require.NoError(t, err) + got := deriveScopedStatusContexts("org/src", "Deploy", content, events) + assert.Empty(t, got) // workflow_dispatch posts no commit status -> nothing to preview (and it cannot be a required check) + }) +} diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index e4fd464375..85ca8acccb 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -116,13 +116,13 @@ func FindOwnerProfileReadme(ctx *context.Context, doer *user_model.User, optProf return nil, nil } - commit, err := profileGitRepo.GetBranchCommit(profileDbRepo.DefaultBranch) + commit, err := profileGitRepo.GetBranchCommit(ctx, profileDbRepo.DefaultBranch) if err != nil { log.Error("FindOwnerProfileReadme failed to GetBranchCommit: %v", err) return nil, nil } - profileReadmeBlob, _ = commit.GetBlobByPath("README.md") // no need to handle this error + profileReadmeBlob, _ = commit.GetBlobByPath(ctx, profileGitRepo, "README.md") // no need to handle this error return profileDbRepo, profileReadmeBlob } diff --git a/routers/web/shared/user/helper.go b/routers/web/shared/user/helper.go index 129b500376..0b3db67091 100644 --- a/routers/web/shared/user/helper.go +++ b/routers/web/shared/user/helper.go @@ -11,22 +11,44 @@ import ( "gitea.dev/models/user" ) +type SearchUserInfo struct { + UserID int64 `json:"user_id"` + UserName string `json:"username"` + AvatarLink string `json:"avatar_link"` + FullName string `json:"full_name"` +} + +type SearchUserResponse struct { + Results []*SearchUserInfo `json:"results"` +} + func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User { - if doer != nil { - idx := slices.IndexFunc(users, func(u *user.User) bool { - return u.ID == doer.ID - }) - if idx > 0 { - newUsers := make([]*user.User, len(users)) - newUsers[0] = users[idx] - copy(newUsers[1:], users[:idx]) - copy(newUsers[idx+1:], users[idx+1:]) - return newUsers - } + if doer == nil { + return users + } + idx := slices.IndexFunc(users, func(u *user.User) bool { + return u.ID == doer.ID + }) + if idx > 0 { + newUsers := make([]*user.User, len(users)) + newUsers[0] = users[idx] + copy(newUsers[1:], users[:idx]) + copy(newUsers[idx+1:], users[idx+1:]) + return newUsers } return users } +func ToSearchUserResponse(ctx context.Context, self *user.User, users []*user.User) (ret *SearchUserResponse) { + infos := MakeSelfOnTop(self, users) + resp := &SearchUserResponse{Results: make([]*SearchUserInfo, len(infos))} + for i, u := range infos { + resp.Results[i] = &SearchUserInfo{UserID: u.ID, UserName: u.Name, AvatarLink: u.AvatarLink(ctx)} + resp.Results[i].FullName = u.FullName + } + return resp +} + // GetFilterUserIDByName tries to get the user ID from the given username. // Before, the "issue filter" passes user ID to query the list, but in many cases, it's impossible to pre-fetch the full user list. // So it's better to make it work like GitHub: users could input username directly. diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 6951be45ef..75b51151f3 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -242,13 +242,13 @@ func Milestones(ctx *context.Context) { } sort.Sort(showRepos) + repoByID := make(map[int64]*repo_model.Repository, len(showRepos)) + for _, repo := range showRepos { + repoByID[repo.ID] = repo + } + for i := 0; i < len(milestones); { - for _, repo := range showRepos { - if milestones[i].RepoID == repo.ID { - milestones[i].Repo = repo - break - } - } + milestones[i].Repo = repoByID[milestones[i].RepoID] if milestones[i].Repo == nil { log.Warn("Cannot find milestone %d 's repository %d", milestones[i].ID, milestones[i].RepoID) milestones = append(milestones[:i], milestones[i+1:]...) diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go index 95c66bd2ea..b82b32f687 100644 --- a/routers/web/user/notification.go +++ b/routers/web/user/notification.go @@ -4,6 +4,7 @@ package user import ( + stdCtx "context" "fmt" "net/http" "strings" @@ -12,8 +13,10 @@ import ( "gitea.dev/models/db" git_model "gitea.dev/models/git" issues_model "gitea.dev/models/issues" + access_model "gitea.dev/models/perm/access" repo_model "gitea.dev/models/repo" "gitea.dev/models/unit" + user_model "gitea.dev/models/user" "gitea.dev/modules/base" "gitea.dev/modules/container" "gitea.dev/modules/log" @@ -97,6 +100,12 @@ func prepareUserNotificationsData(ctx *context.Context) { return } failCount += len(failures) + notifications, failures, err = filterNotificationsByRepoAccess(ctx, ctx.Doer, notifications) + if err != nil { + ctx.ServerError("filterNotificationsByRepoAccess", err) + return + } + failCount += len(failures) failures, err = notifications.LoadIssues(ctx) if err != nil { @@ -135,6 +144,23 @@ func prepareUserNotificationsData(ctx *context.Context) { ctx.Data["Page"] = pager } +func filterNotificationsByRepoAccess(ctx stdCtx.Context, doer *user_model.User, notifications activities_model.NotificationList) (activities_model.NotificationList, []int, error) { + failures := make([]int, 0) + for i, notification := range notifications { + if notification.Repository == nil { + continue + } + perm, err := access_model.GetIndividualUserRepoPermission(ctx, notification.Repository, doer) + if err != nil { + return nil, nil, err + } + if !perm.HasAnyUnitAccessOrPublicAccess() { + failures = append(failures, i) + } + } + return notifications.Without(failures), failures, nil +} + // NotificationStatusPost is a route for changing the status of a notification func NotificationStatusPost(ctx *context.Context) { notificationID := ctx.FormInt64("notification_id") diff --git a/routers/web/user/notification_test.go b/routers/web/user/notification_test.go new file mode 100644 index 0000000000..b6c060d04c --- /dev/null +++ b/routers/web/user/notification_test.go @@ -0,0 +1,37 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "testing" + + activities_model "gitea.dev/models/activities" + repo_model "gitea.dev/models/repo" + "gitea.dev/models/unittest" + user_model "gitea.dev/models/user" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFilterNotificationsByRepoAccess(t *testing.T) { + require.NoError(t, unittest.LoadFixtures()) + + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40}) + inaccessibleRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) + require.True(t, inaccessibleRepo.IsPrivate) + accessibleRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + notifications := activities_model.NotificationList{ + {ID: 1, Repository: inaccessibleRepo}, + {ID: 2, Repository: accessibleRepo}, + } + + filtered, failures, err := filterNotificationsByRepoAccess(t.Context(), doer, notifications) + require.NoError(t, err) + + assert.Equal(t, []int{0}, failures) + require.Len(t, filtered, 1) + assert.EqualValues(t, 2, filtered[0].ID) +} diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 654bd1a5f0..8efb5e5945 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -170,6 +170,10 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R date := ctx.FormString("date") pagingNum = setting.UI.FeedPagingNum showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) + // a public-only API token must not surface private activity, even for its own owner + if showPrivate && context.TokenIsPublicOnly(ctx) { + showPrivate = false + } items, feedCount, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{ RequestedUser: ctx.ContextUser, Actor: ctx.Doer, @@ -247,7 +251,7 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R total = count case "overview": - if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { + if bytes, err := profileReadme.GetBlobContent(ctx, setting.UI.MaxDisplayFileSize); err != nil { log.Error("failed to GetBlobContent: %v", err) } else { rctx := renderhelper.NewRenderContextRepoFile(ctx, profileDbRepo, renderhelper.RepoFileOptions{ diff --git a/routers/web/user/setting/adopt.go b/routers/web/user/setting/adopt.go index f2b04872f7..cd3ab7d3b0 100644 --- a/routers/web/user/setting/adopt.go +++ b/routers/web/user/setting/adopt.go @@ -32,7 +32,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) { return } - exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, dir))) + exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.CodeRepoByName(ctxUser.Name, dir)) if err != nil { ctx.ServerError("IsDir", err) return diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go index c50b96adb4..c2adf2c695 100644 --- a/routers/web/user/setting/applications.go +++ b/routers/web/user/setting/applications.go @@ -79,6 +79,30 @@ func ApplicationsPost(ctx *context.Context) { return } + // a token-authenticated request must not mint a token with a broader scope than its own, nor + // drop the public-only restriction. Web routes accept basic-auth PATs/OAuth tokens too, so this + // must mirror the REST API guard in routers/api/v1/user/app.go. + if ctx.Data["IsApiToken"] == true { + apiTokenScope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) + if !ok { + ctx.HTTPError(http.StatusForbidden, "the authenticating token has no scope") + return + } + hasScope, err := apiTokenScope.CanCreateChildScope(t.Scope) + if err != nil { + ctx.ServerError("CanCreateChildScope", err) + return + } + if !hasScope { + ctx.HTTPError(http.StatusForbidden, "cannot create an access token with a broader scope than the authenticating token") + return + } + if t.Scope, err = t.Scope.EnforcePublicOnlyFrom(apiTokenScope); err != nil { + ctx.ServerError("EnforcePublicOnlyFrom", err) + return + } + } + if err := auth_model.NewAccessToken(ctx, t); err != nil { ctx.ServerError("NewAccessToken", err) return diff --git a/routers/web/web.go b/routers/web/web.go index 4525516f5a..0bfae44e86 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -500,6 +500,15 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { }) } + addSettingsScopedWorkflowsRoutes := func() { + m.Group("/scoped-workflows", func() { + m.Get("", shared_actions.ScopedWorkflows) + m.Post("/add", shared_actions.ScopedWorkflowAdd) + m.Post("/required", shared_actions.ScopedWorkflowSetRequired) + m.Post("/remove", shared_actions.ScopedWorkflowRemove) + }) + } + // FIXME: not all routes need go through same middleware. // Especially some AJAX requests, we can reduce middleware number to improve performance. @@ -702,6 +711,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { addSettingsRunnersRoutes() addSettingsSecretsRoutes() addSettingsVariablesRoutes() + addSettingsScopedWorkflowsRoutes() }, actions.MustEnableActions) m.Get("/organization", user_setting.Organization) @@ -865,6 +875,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { addSettingsRunnersRoutes() m.Post("/runners/bulk", shared_actions.RunnerBulkActionPost) addSettingsVariablesRoutes() + addSettingsScopedWorkflowsRoutes() }) }, adminReq, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled)) // ***** END: Admin ***** @@ -1022,6 +1033,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { addSettingsRunnersRoutes() addSettingsSecretsRoutes() addSettingsVariablesRoutes() + addSettingsScopedWorkflowsRoutes() }, actions.MustEnableActions) m.Post("/rename", web.Bind(forms.RenameOrgForm{}), org.SettingsRenamePost) diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go index 2b8ed19f92..dc999f323d 100644 --- a/services/actions/commit_status.go +++ b/services/actions/commit_status.go @@ -7,8 +7,7 @@ import ( "context" "errors" "fmt" - "path" - "strings" + "slices" actions_model "gitea.dev/models/actions" "gitea.dev/models/db" @@ -18,9 +17,12 @@ import ( actions_module "gitea.dev/modules/actions" "gitea.dev/modules/actions/jobparser" "gitea.dev/modules/commitstatus" + "gitea.dev/modules/glob" "gitea.dev/modules/log" + api "gitea.dev/modules/structs" "gitea.dev/modules/util" webhook_module "gitea.dev/modules/webhook" + pull_service "gitea.dev/services/pull" commitstatus_service "gitea.dev/services/repository/commitstatus" ) @@ -45,8 +47,14 @@ func CreateCommitStatusForRunJobs(ctx context.Context, run *actions_model.Action return } + // Compute the scoped source-repo prefix once per run; it is identical for every job. + var scopedPrefix string + if run.IsScopedRun { + scopedPrefix = actions_model.ScopedStatusContextPrefix(ctx, run.WorkflowRepoID) + } + for _, job := range jobs { - if err = createCommitStatus(ctx, run.Repo, event, commitID, run, job); err != nil { + if err = createCommitStatus(ctx, run.Repo, event, commitID, scopedPrefix, run, job); err != nil { log.Error("Failed to create commit status for job %d: %v", job.ID, err) } } @@ -136,30 +144,121 @@ func getCommitStatusEventNameAndCommitID(run *actions_model.ActionRun) (event, c return event, commitID, nil } -func createCommitStatus(ctx context.Context, repo *repo_model.Repository, event, commitID string, run *actions_model.ActionRun, job *actions_model.ActionRunJob) error { - // TODO: store workflow name as a field in ActionRun to avoid parsing - runName := path.Base(run.WorkflowID) - // fall back to the file name when the workflow has no non-blank `name:` - if wfs, err := jobparser.Parse(job.WorkflowPayload); err == nil && len(wfs) > 0 { - if name := strings.TrimSpace(wfs[0].Name); name != "" { - runName = name +func createCommitStatus(ctx context.Context, repo *repo_model.Repository, event, commitID, scopedPrefix string, run *actions_model.ActionRun, job *actions_model.ActionRunJob) error { + displayName := actions_module.WorkflowDisplayName(run.WorkflowID, job.WorkflowPayload) + ctxName := actions_module.WorkflowStatusContextName(displayName, job.Name, event) // git_model.NewCommitStatus also trims spaces + if run.IsScopedRun { + // A scoped run is prefixed with its source repo (set off by a colon) so it stays distinct from a same-named repo-level workflow. + // scopedPrefix is computed once per run by the caller. The settings page derives the same string to preview expected checks. + ctxName = actions_module.ScopedWorkflowStatusContextName(scopedPrefix, displayName, job.Name, event) + } + targetURL := fmt.Sprintf("%s/jobs/%d", run.Link(), job.ID) + return createWorkflowCommitStatus(ctx, repo, commitID, ctxName, run.WorkflowID, toCommitStatus(job.Status), targetURL, toCommitStatusDescription(job)) +} + +// getAllRequiredStatusContextGlobs returns the compiled globs of every status-check context required in the repo: +// its protected branch rules' contexts and its required scoped workflows' patterns (see EffectiveRequiredContexts). +func getAllRequiredStatusContextGlobs(ctx context.Context, repo *repo_model.Repository) ([]glob.Glob, error) { + rules, err := git_model.FindRepoProtectedBranchRules(ctx, repo.ID) + if err != nil { + return nil, fmt.Errorf("FindRepoProtectedBranchRules: %w", err) + } + required, err := pull_service.EffectiveRequiredContexts(ctx, repo, rules...) + if err != nil { + return nil, fmt.Errorf("EffectiveRequiredContexts: %w", err) + } + globs := make([]glob.Glob, 0, len(required)) + for _, c := range required { + if gp, err := glob.Compile(c); err != nil { + log.Error("glob.Compile %q: %v", c, err) + } else { + globs = append(globs, gp) } } - ctxName := strings.TrimSpace(fmt.Sprintf("%s / %s (%s)", runName, job.Name, event)) // git_model.NewCommitStatus also trims spaces + return globs, nil +} + +// CreateSkippedCommitStatusForFilteredWorkflow posts a skipped commit status for each job of a +// workflow that matched the triggering event but was excluded by a branch/paths filter. +// This lets a required status check tied to that context be satisfied without the workflow running. +// Only contexts matching requiredGlobs are posted; a non-required context gets no skipped status. +// No ActionRun is created, so the status has no target URL (there is no run/job to link to). +// A non-empty scopedPrefix prefixes each context with its source repo, matching scoped runs. +// +// TODO: requiredGlobs over-approximates by including every protected branch rule's required contexts. +// If possible, the set should be narrowed to the required contexts of a specific branch protection rule (and the contexts from required scoped workflows). +// Currently, a `push` event must keep the repo-wide union since its future pull request base branch is unknown. +func CreateSkippedCommitStatusForFilteredWorkflow(ctx context.Context, repo *repo_model.Repository, event webhook_module.HookEventType, triggerEvent, workflowID string, content []byte, payload api.Payloader, scopedPrefix string, requiredGlobs []glob.Glob) error { + if len(requiredGlobs) == 0 { + return nil // nothing is required, so no skipped status is needed + } + + // Derive the status event name and target commit from the payload. + // TODO: this mirrors getCommitStatusEventNameAndCommitID, which derives the same from a persisted run. Should merge the logic if possible. + var statusEvent, commitID string + switch event { + case webhook_module.HookEventPush: + if p, ok := payload.(*api.PushPayload); ok && p.HeadCommit != nil { + statusEvent, commitID = "push", p.HeadCommit.ID + } + case webhook_module.HookEventPullRequest, + webhook_module.HookEventPullRequestSync, + webhook_module.HookEventPullRequestAssign, + webhook_module.HookEventPullRequestLabel, + webhook_module.HookEventPullRequestReviewRequest, + webhook_module.HookEventPullRequestMilestone: + if p, ok := payload.(*api.PullRequestPayload); ok && p.PullRequest != nil && p.PullRequest.Head != nil { + statusEvent, commitID = "pull_request", p.PullRequest.Head.Sha + if triggerEvent == actions_module.GithubEventPullRequestTarget { + statusEvent = "pull_request_target" + } + } + } + if statusEvent == "" || commitID == "" { + return nil // unsupported event or missing commit id, nothing to post + } + + workflows, err := jobparser.Parse(content) + if err != nil { + return fmt.Errorf("jobparser.Parse: %w", err) + } + + displayName := actions_module.WorkflowDisplayName(workflowID, content) + for _, sw := range workflows { + _, job := sw.Job() + if job == nil { + continue + } + jobName := util.EllipsisDisplayString(job.Name, 255) // run creation truncates job names the same way + ctxName := actions_module.WorkflowStatusContextName(displayName, jobName, statusEvent) + if scopedPrefix != "" { + ctxName = actions_module.ScopedWorkflowStatusContextName(scopedPrefix, displayName, jobName, statusEvent) + } + // Only a required context needs the skipped status. + if !slices.ContainsFunc(requiredGlobs, func(gp glob.Glob) bool { return gp.Match(ctxName) }) { + continue + } + // "Skipped" mirrors toCommitStatusDescription for StatusSkipped. + if err := createWorkflowCommitStatus(ctx, repo, commitID, ctxName, workflowID, commitstatus.CommitStatusSkipped, "", "Skipped"); err != nil { + return err + } + } + return nil +} + +// createWorkflowCommitStatus posts the commit status for one workflow-job context. +func createWorkflowCommitStatus(ctx context.Context, repo *repo_model.Repository, commitID, ctxName, workflowID string, state commitstatus.CommitStatusState, targetURL, description string) error { // Mix the workflow file path into the hash so two workflow files that // share the same `name:` and job name produce distinct commit statuses // even though they render identically — matching GitHub's behavior // (issue #35699). - ctxHash := git_model.HashCommitStatusContext(ctxName + "\x00" + run.WorkflowID) + ctxHash := git_model.HashCommitStatusContext(ctxName + "\x00" + workflowID) // Pre-fix rows were hashed from Context alone. If a pre-existing row with // the legacy hash is still the "latest" for this SHA, reuse that hash so // the new row supersedes it; otherwise the old pending status would stay // stuck forever (it lives in its own dedupe group). Only relevant for // in-flight workflows at upgrade time. legacyHash := git_model.HashCommitStatusContext(ctxName) - state := toCommitStatus(job.Status) - targetURL := fmt.Sprintf("%s/jobs/%d", run.Link(), job.ID) - description := toCommitStatusDescription(job) statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commitID, db.ListOptionsAll) if err != nil { diff --git a/services/actions/commit_status_test.go b/services/actions/commit_status_test.go index e20dcf8511..151bf2e598 100644 --- a/services/actions/commit_status_test.go +++ b/services/actions/commit_status_test.go @@ -48,11 +48,11 @@ func TestCreateCommitStatus_Dedupe(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) - gitRepo, err := gitrepo.OpenRepository(t.Context(), repo) + gitRepo, err := gitrepo.OpenRepository(repo) require.NoError(t, err) defer gitRepo.Close() - commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch) + commit, err := gitRepo.GetBranchCommit(t.Context(), repo.DefaultBranch) require.NoError(t, err) run := &actions_model.ActionRun{ @@ -72,7 +72,7 @@ func TestCreateCommitStatus_Dedupe(t *testing.T) { expectedContext := "status-dedupe-test.yaml / status-dedupe-job (push)" expectedTargetURL := run.Link() + "/jobs/99002" - require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), run, job)) + require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), "", run, job)) statuses := findCommitStatusesForContext(t, repo.ID, commit.ID.String(), expectedContext) require.Len(t, statuses, 1) @@ -81,7 +81,7 @@ func TestCreateCommitStatus_Dedupe(t *testing.T) { assert.Equal(t, expectedTargetURL, statuses[0].TargetURL) job.Status = actions_model.StatusRunning - require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), run, job)) + require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), "", run, job)) statuses = findCommitStatusesForContext(t, repo.ID, commit.ID.String(), expectedContext) require.Len(t, statuses, 2) @@ -90,12 +90,12 @@ func TestCreateCommitStatus_Dedupe(t *testing.T) { assert.Equal(t, "In progress", statuses[1].Description) assert.Equal(t, expectedTargetURL, statuses[1].TargetURL) - require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), run, job)) + require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), "", run, job)) statuses = findCommitStatusesForContext(t, repo.ID, commit.ID.String(), expectedContext) assert.Len(t, statuses, 2) job.Status = actions_model.StatusSuccess - require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), run, job)) + require.NoError(t, createCommitStatus(t.Context(), repo, "push", commit.ID.String(), "", run, job)) statuses = findCommitStatusesForContext(t, repo.ID, commit.ID.String(), expectedContext) require.Len(t, statuses, 3) assert.Equal(t, commitstatus.CommitStatusSuccess, statuses[2].State) @@ -126,7 +126,7 @@ func TestGetCommitActionsStatusMap(t *testing.T) { RunID: run.ID, RepoID: repo.ID, OwnerID: repo.OwnerID, Name: tc.jobName, Status: tc.status, } require.NoError(t, db.Insert(t.Context(), job)) - require.NoError(t, createCommitStatus(t.Context(), repo, "push", branch.CommitID, run, job)) + require.NoError(t, createCommitStatus(t.Context(), repo, "push", branch.CommitID, "", run, job)) } statuses, err := git_model.GetLatestCommitStatus(t.Context(), repo.ID, branch.CommitID, db.ListOptionsAll) @@ -185,7 +185,7 @@ jobs: WorkflowPayload: payload, } require.NoError(t, db.Insert(t.Context(), job)) - require.NoError(t, createCommitStatus(t.Context(), repo, "pull_request", branch.CommitID, run, job)) + require.NoError(t, createCommitStatus(t.Context(), repo, "pull_request", branch.CommitID, "", run, job)) } statuses, err := git_model.GetLatestCommitStatus(t.Context(), repo.ID, branch.CommitID, db.ListOptionsAll) @@ -242,7 +242,7 @@ func TestCreateCommitStatus_LegacyHashRecovery(t *testing.T) { Name: "my-job", Status: actions_model.StatusSuccess, } require.NoError(t, db.Insert(t.Context(), job)) - require.NoError(t, createCommitStatus(t.Context(), repo, "push", branch.CommitID, run, job)) + require.NoError(t, createCommitStatus(t.Context(), repo, "push", branch.CommitID, "", run, job)) latest, err := git_model.GetLatestCommitStatus(t.Context(), repo.ID, branch.CommitID, db.ListOptionsAll) require.NoError(t, err) @@ -292,7 +292,7 @@ func TestCreateCommitStatus_UnnamedWorkflowUsesFileName(t *testing.T) { `), } require.NoError(t, db.Insert(t.Context(), job)) - require.NoError(t, createCommitStatus(t.Context(), repo, "push", branch.CommitID, run, job)) + require.NoError(t, createCommitStatus(t.Context(), repo, "push", branch.CommitID, "", run, job)) statuses := findCommitStatusesForContext(t, repo.ID, branch.CommitID, tc.workflowID+" / my-test (push)") require.Len(t, statuses, 1) @@ -300,6 +300,67 @@ func TestCreateCommitStatus_UnnamedWorkflowUsesFileName(t *testing.T) { } } +// TestCreateCommitStatus_ScopedSourcePrefix: a scoped run's commit status Context is prefixed with the source repo's full name, +// so it is distinct (display AND hash) from a same-named repo-level workflow. +func TestCreateCommitStatus_ScopedSourcePrefix(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + consumer := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) + source := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + branch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: consumer.ID, Name: consumer.DefaultBranch}) + + payload := []byte(`name: ci +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo hi +`) + + // A repo-level run and a scoped run share the same workflow name and job name; + // only the scoped one points its content source at another repo (WorkflowRepoID=source.ID, IsScopedRun=true). + for _, spec := range []struct { + runID, jobID int64 + scoped bool + }{ + {99501, 99511, false}, + {99502, 99512, true}, + } { + workflowRepoID := consumer.ID + if spec.scoped { + workflowRepoID = source.ID + } + run := &actions_model.ActionRun{ + ID: spec.runID, Index: spec.runID, RepoID: consumer.ID, Repo: consumer, OwnerID: consumer.OwnerID, TriggerUserID: consumer.OwnerID, + WorkflowID: "ci.yaml", CommitSHA: branch.CommitID, + WorkflowRepoID: workflowRepoID, WorkflowCommitSHA: branch.CommitID, IsScopedRun: spec.scoped, + } + require.NoError(t, db.Insert(t.Context(), run)) + job := &actions_model.ActionRunJob{ + ID: spec.jobID, RunID: run.ID, RepoID: consumer.ID, OwnerID: consumer.OwnerID, + Name: "build", Status: actions_model.StatusWaiting, WorkflowPayload: payload, + } + require.NoError(t, db.Insert(t.Context(), job)) + // mirror CreateCommitStatusForRunJobs: compute the scoped prefix once per run + scopedPrefix := "" + if run.IsScopedRun { + scopedPrefix = actions_model.ScopedStatusContextPrefix(t.Context(), run.WorkflowRepoID) + } + require.NoError(t, createCommitStatus(t.Context(), consumer, "push", branch.CommitID, scopedPrefix, run, job)) + } + + // repo-level Context is the bare " / ()"; the scoped one is the same but sets off the source repo with a colon, + // so the two stay distinct (and have different hashes) despite the same `name:`. + repoStatuses := findCommitStatusesForContext(t, consumer.ID, branch.CommitID, "ci / build (push)") + require.Len(t, repoStatuses, 1) + scopedStatuses := findCommitStatusesForContext(t, consumer.ID, branch.CommitID, source.FullName()+": ci / build (push)") + require.Len(t, scopedStatuses, 1) + + assert.NotEqual(t, repoStatuses[0].ContextHash, scopedStatuses[0].ContextHash, + "scoped status must not collide with the same-named repo-level workflow") +} + func findCommitStatusesForContext(t *testing.T, repoID int64, sha, context string) []*git_model.CommitStatus { t.Helper() diff --git a/services/actions/context_test.go b/services/actions/context_test.go index 252399a37d..8100f1e238 100644 --- a/services/actions/context_test.go +++ b/services/actions/context_test.go @@ -63,16 +63,18 @@ jobs: `) run := &actions_model.ActionRun{ - Title: "before parse", - RepoID: 4, - OwnerID: 1, - WorkflowID: "expr-runid.yaml", - TriggerUserID: 1, - Ref: "refs/heads/master", - CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0", - Event: "push", - TriggerEvent: "push", - EventPayload: "{}", + Title: "before parse", + RepoID: 4, + OwnerID: 1, + WorkflowID: "expr-runid.yaml", + TriggerUserID: 1, + Ref: "refs/heads/master", + CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0", + Event: "push", + TriggerEvent: "push", + EventPayload: "{}", + WorkflowRepoID: 4, + WorkflowCommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0", } require.NoError(t, PrepareRunAndInsert(ctx, content, run, nil)) require.Positive(t, run.ID) diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go index 9c9a408db0..1b45eab83f 100644 --- a/services/actions/job_emitter.go +++ b/services/actions/job_emitter.go @@ -380,6 +380,11 @@ func (r *jobStatusResolver) resolveCheckNeeds(id int64) (allDone, allSucceed boo if !needStatus.IsDone() { allDone = false } + // A failed need with continue-on-error:true is treated as success, matching AggregateJobStatus, + // so a downstream job with an implicit `success()` is not skipped. + if needJob := r.jobMap[need]; needJob != nil && needJob.ContinueOnError && needStatus == actions_model.StatusFailure { + continue + } if needStatus.In(actions_model.StatusFailure, actions_model.StatusCancelled, actions_model.StatusSkipped) { allSucceed = false } diff --git a/services/actions/job_emitter_test.go b/services/actions/job_emitter_test.go index 892ba6c2c5..6b15ecd346 100644 --- a/services/actions/job_emitter_test.go +++ b/services/actions/job_emitter_test.go @@ -131,6 +131,24 @@ jobs: }, want: map[int64]actions_model.Status{2: actions_model.StatusSkipped}, }, + { + name: "`if` is empty and a failed need has continue-on-error", + jobs: actions_model.ActionJobList{ + {ID: 1, JobID: "job1", Status: actions_model.StatusFailure, ContinueOnError: true, Needs: []string{}}, + {ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte( + ` +name: test +on: push +jobs: + job2: + runs-on: ubuntu-latest + needs: job1 + steps: + - run: echo "should run, job1 failure is masked by continue-on-error" +`)}, + }, + want: map[int64]actions_model.Status{2: actions_model.StatusWaiting}, + }, } assert.NoError(t, unittest.PrepareTestDatabase()) ctx := t.Context() diff --git a/services/actions/notifier.go b/services/actions/notifier.go index 5670c2a634..ae8ff2b0e7 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -16,7 +16,6 @@ import ( repo_model "gitea.dev/models/repo" user_model "gitea.dev/models/user" "gitea.dev/modules/git" - "gitea.dev/modules/gitrepo" "gitea.dev/modules/log" "gitea.dev/modules/repository" "gitea.dev/modules/setting" @@ -802,21 +801,17 @@ func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *rep status := convert.ToWorkflowRunAction(run.Status) - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + convertedWorkflow, err := convert.ResolveActionWorkflowForRun(ctx, repo, run) if err != nil { - log.Error("OpenRepository: %v", err) + if errors.Is(err, util.ErrNotExist) { + // The workflow definition is gone (e.g. a scoped source repo/file was deleted, or the file no longer exists at the recorded commit), skip + log.Debug("WorkflowRunStatusUpdate: workflow %q for run %d not found: %v", run.WorkflowID, run.ID, err) + return + } + log.Error("WorkflowRunStatusUpdate resolve workflow: %v", err) return } - defer gitRepo.Close() - convertedWorkflow, err := convert.GetActionWorkflowByRef(ctx, gitRepo, repo, run.WorkflowID, git.RefName(run.Ref)) - if err != nil && errors.Is(err, util.ErrNotExist) { - convertedWorkflow, err = convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) - } - if err != nil { - log.Error("GetActionWorkflow: %v", err) - return - } run.Repo = repo convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil, false) if err != nil { diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index e67d1028f7..4721b58dee 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -19,6 +19,7 @@ import ( unit_model "gitea.dev/models/unit" user_model "gitea.dev/models/user" actions_module "gitea.dev/modules/actions" + "gitea.dev/modules/container" "gitea.dev/modules/git" "gitea.dev/modules/gitrepo" "gitea.dev/modules/json" @@ -147,7 +148,7 @@ func notify(ctx context.Context, input *notifyInput) error { return nil } - gitRepo, err := gitrepo.OpenRepository(context.Background(), input.Repo) + gitRepo, err := gitrepo.OpenRepository(input.Repo) if err != nil { return fmt.Errorf("git.OpenRepository: %w", err) } @@ -166,13 +167,13 @@ func notify(ctx context.Context, input *notifyInput) error { ref = git.RefNameFromBranch(input.Repo.DefaultBranch) } - commitID, err := gitRepo.GetRefCommitID(ref.String()) + commitID, err := gitRepo.GetRefCommitID(ctx, ref.String()) if err != nil { return fmt.Errorf("gitRepo.GetRefCommitID: %w", err) } // Get the commit object for the ref - commit, err := gitRepo.GetCommit(commitID) + commit, err := gitRepo.GetCommit(ctx, commitID) if err != nil { return fmt.Errorf("gitRepo.GetCommit: %w", err) } @@ -182,8 +183,9 @@ func notify(ctx context.Context, input *notifyInput) error { } var detectedWorkflows []*actions_module.DetectedWorkflow + var filteredWorkflows []*actions_module.DetectedWorkflow actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig() - workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit, + workflows, schedules, filtered, err := actions_module.DetectWorkflows(ctx, gitRepo, commit, input.Event, input.Payload, shouldDetectSchedules, @@ -193,7 +195,7 @@ func notify(ctx context.Context, input *notifyInput) error { } log.Trace("repo %s with commit %s event %s find %d workflows and %d schedules", - input.Repo.RelativePath(), + input.Repo.FullName(), commit.ID, input.Event, len(workflows), @@ -202,7 +204,7 @@ func notify(ctx context.Context, input *notifyInput) error { for _, wf := range workflows { if actionsConfig.IsWorkflowDisabled(wf.EntryName) { - log.Trace("repo %s has disable workflows %s", input.Repo.RelativePath(), wf.EntryName) + log.Trace("repo %s has disable workflows %s", input.Repo.FullName(), wf.EntryName) continue } @@ -211,26 +213,50 @@ func notify(ctx context.Context, input *notifyInput) error { } } + for _, wf := range filtered { + if actionsConfig.IsWorkflowDisabled(wf.EntryName) { + log.Trace("repo %s has disable workflows %s", input.Repo.FullName(), wf.EntryName) + continue + } + + if wf.TriggerEvent.Name != actions_module.GithubEventPullRequestTarget { + filteredWorkflows = append(filteredWorkflows, wf) + } + } + if input.PullRequest != nil { // detect pull_request_target workflows baseRef := git.BranchPrefix + input.PullRequest.BaseBranch - baseCommit, err := gitRepo.GetCommit(baseRef) + baseCommit, err := gitRepo.GetCommit(ctx, baseRef) if err != nil { return fmt.Errorf("gitRepo.GetCommit: %w", err) } - baseWorkflows, _, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload, false) + baseWorkflows, _, baseFiltered, err := actions_module.DetectWorkflows(ctx, gitRepo, baseCommit, input.Event, input.Payload, false) if err != nil { return fmt.Errorf("DetectWorkflows: %w", err) } if len(baseWorkflows) == 0 { - log.Trace("repo %s with commit %s couldn't find pull_request_target workflows", input.Repo.RelativePath(), baseCommit.ID) + log.Trace("repo %s with commit %s couldn't find pull_request_target workflows", input.Repo.FullName(), baseCommit.ID) } else { for _, wf := range baseWorkflows { + if actionsConfig.IsWorkflowDisabled(wf.EntryName) { + log.Trace("repo %s has disable workflows %s", input.Repo.FullName(), wf.EntryName) + continue + } if wf.TriggerEvent.Name == actions_module.GithubEventPullRequestTarget { detectedWorkflows = append(detectedWorkflows, wf) } } } + for _, wf := range baseFiltered { + if actionsConfig.IsWorkflowDisabled(wf.EntryName) { + log.Trace("repo %s has disable workflows %s", input.Repo.FullName(), wf.EntryName) + continue + } + if wf.TriggerEvent.Name == actions_module.GithubEventPullRequestTarget { + filteredWorkflows = append(filteredWorkflows, wf) + } + } } if shouldDetectSchedules { @@ -239,7 +265,13 @@ func notify(ctx context.Context, input *notifyInput) error { } } - return handleWorkflows(ctx, detectedWorkflows, commit, input, ref) + if err := handleWorkflows(ctx, detectedWorkflows, commit, input, ref); err != nil { + return err + } + + handleFilteredWorkflows(ctx, input, filteredWorkflows) + + return detectAndHandleScopedWorkflows(ctx, input, ref, gitRepo, commit) } func skipWorkflows(ctx context.Context, input *notifyInput, commit *git.Commit) bool { @@ -253,11 +285,11 @@ func skipWorkflows(ctx context.Context, input *notifyInput, commit *git.Commit) if slices.Contains(skipWorkflowEvents, input.Event) { for _, s := range setting.Actions.SkipWorkflowStrings { if input.PullRequest != nil && strings.Contains(input.PullRequest.Issue.Title, s) { - log.Debug("repo %s: skipped run for pr %v because of %s string", input.Repo.RelativePath(), input.PullRequest.Issue.ID, s) + log.Debug("repo %s: skipped run for pr %v because of %s string", input.Repo.FullName(), input.PullRequest.Issue.ID, s) return true } if strings.Contains(commit.MessageRaw, s) { - log.Debug("repo %s with commit %s: skipped run because of %s string", input.Repo.RelativePath(), commit.ID, s) + log.Debug("repo %s with commit %s: skipped run because of %s string", input.Repo.FullName(), commit.ID, s) return true } } @@ -280,7 +312,7 @@ func skipWorkflows(ctx context.Context, input *notifyInput, commit *git.Commit) } } // skip workflow runs events exceeding the maximum of 5 recursive events - log.Debug("repo %s: skipped workflow_run because of recursive event of 5", input.Repo.RelativePath()) + log.Debug("repo %s: skipped workflow_run because of recursive event of 5", input.Repo.FullName()) return true } return false @@ -294,7 +326,7 @@ func handleWorkflows( ref git.RefName, ) error { if len(detectedWorkflows) == 0 { - log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RelativePath(), commit.ID) + log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.FullName(), commit.ID) return nil } @@ -303,55 +335,89 @@ func handleWorkflows( return fmt.Errorf("json.Marshal: %w", err) } - isForkPullRequest := false - if pr := input.PullRequest; pr != nil { - switch pr.Flow { - case issues_model.PullRequestFlowGithub: - isForkPullRequest = pr.IsFromFork() - case issues_model.PullRequestFlowAGit: - // There is no fork concept in agit flow, anyone with read permission can push refs/for// to the repo. - // So we can treat it as a fork pull request because it may be from an untrusted user - isForkPullRequest = true - default: - // unknown flow, assume it's a fork pull request to be safe - isForkPullRequest = true - } - } + isForkPullRequest := isForkPullRequestInput(input) for _, dwf := range detectedWorkflows { - run := &actions_model.ActionRun{ - Title: commit.MessageTitle(), - RepoID: input.Repo.ID, - Repo: input.Repo, - OwnerID: input.Repo.OwnerID, - WorkflowID: dwf.EntryName, - TriggerUserID: input.Doer.ID, - TriggerUser: input.Doer, - Ref: ref.String(), - CommitSHA: commit.ID.String(), - IsForkPullRequest: isForkPullRequest, - Event: input.Event, - EventPayload: string(p), - TriggerEvent: dwf.TriggerEvent.Name, - Status: actions_model.StatusWaiting, - } - - need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer) - if err != nil { - log.Error("check if need approval for repo %d with user %d: %v", input.Repo.ID, input.Doer.ID, err) - continue - } - - run.NeedApproval = need - - if err := PrepareRunAndInsert(ctx, dwf.Content, run, nil); err != nil { - log.Error("PrepareRunAndInsert: %v", err) + // repo-level run: the workflow content is this repo at this commit + if err := buildApproveAndInsertRun(ctx, input, ref, commit, string(p), isForkPullRequest, dwf, input.Repo.ID, commit.ID.String(), false); err != nil { + log.Error("repo %s: %v", input.Repo.FullName(), err) continue } } return nil } +// buildApproveAndInsertRun assembles an ActionRun for a detected workflow, runs the +// fork-PR approval gate, and inserts it. Repo-level and scoped runs share this path so +// run construction and the approval flow have a single implementation that can't drift. +// workflowRepoID/workflowCommitSHA point at the repo+commit the workflow content comes +// from (the repo itself for repo-level runs, the source repo for scoped runs). +func buildApproveAndInsertRun( + ctx context.Context, + input *notifyInput, + ref git.RefName, + commit *git.Commit, + payload string, + isForkPullRequest bool, + dwf *actions_module.DetectedWorkflow, + workflowRepoID int64, + workflowCommitSHA string, + isScopedRun bool, +) error { + run := &actions_model.ActionRun{ + Title: commit.MessageTitle(), + RepoID: input.Repo.ID, + Repo: input.Repo, + OwnerID: input.Repo.OwnerID, + WorkflowID: dwf.EntryName, + TriggerUserID: input.Doer.ID, + TriggerUser: input.Doer, + Ref: ref.String(), + CommitSHA: commit.ID.String(), + IsForkPullRequest: isForkPullRequest, + Event: input.Event, + EventPayload: payload, + TriggerEvent: dwf.TriggerEvent.Name, + Status: actions_model.StatusWaiting, + WorkflowRepoID: workflowRepoID, + WorkflowCommitSHA: workflowCommitSHA, + IsScopedRun: isScopedRun, + } + + need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer) + if err != nil { + return fmt.Errorf("check if need approval for user %d: %w", input.Doer.ID, err) + } + run.NeedApproval = need + + if err := PrepareRunAndInsert(ctx, dwf.Content, run, nil); err != nil { + return fmt.Errorf("PrepareRunAndInsert: %w", err) + } + return nil +} + +// handleFilteredWorkflows posts a skipped commit status for each filtered-out workflow whose context is a required status check; +// a non-required one posts nothing, so it cannot leak into a pull request. +func handleFilteredWorkflows(ctx context.Context, input *notifyInput, filteredWorkflows []*actions_module.DetectedWorkflow) { + if len(filteredWorkflows) == 0 { + return + } + requiredGlobs, err := getAllRequiredStatusContextGlobs(ctx, input.Repo) + if err != nil { + log.Error("repo %s: required status contexts: %v", input.Repo.FullName(), err) + return + } + if len(requiredGlobs) == 0 { + return + } + for _, dwf := range filteredWorkflows { + if err := CreateSkippedCommitStatusForFilteredWorkflow(ctx, input.Repo, input.Event, dwf.TriggerEvent.Name, dwf.EntryName, dwf.Content, input.Payload, "", requiredGlobs); err != nil { + log.Error("repo %s: skipped commit status for workflow %s: %v", input.Repo.FullName(), dwf.EntryName, err) + continue + } + } +} + func newNotifyInputFromIssue(issue *issues_model.Issue, event webhook_module.HookEventType) *notifyInput { return newNotifyInput(issue.Repo, issue.Poster, event) } @@ -474,7 +540,7 @@ func handleSchedules( } if len(detectedWorkflows) == 0 { - log.Trace("repo %s with commit %s couldn't find schedules", input.Repo.RelativePath(), commit.ID) + log.Trace("repo %s with commit %s couldn't find schedules", input.Repo.FullName(), commit.ID) return nil } @@ -525,18 +591,18 @@ func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository) return nil } - gitRepo, err := gitrepo.OpenRepository(context.Background(), repo) + gitRepo, err := gitrepo.OpenRepository(repo) if err != nil { return fmt.Errorf("git.OpenRepository: %w", err) } defer gitRepo.Close() // Only detect schedule workflows on the default branch - commit, err := gitRepo.GetCommit(repo.DefaultBranch) + commit, err := gitRepo.GetCommit(ctx, repo.DefaultBranch) if err != nil { return fmt.Errorf("gitRepo.GetCommit: %w", err) } - scheduleWorkflows, err := actions_module.DetectScheduledWorkflows(gitRepo, commit) + scheduleWorkflows, err := actions_module.DetectScheduledWorkflows(ctx, gitRepo, commit) if err != nil { return fmt.Errorf("detect schedule workflows: %w", err) } @@ -551,3 +617,137 @@ func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository) return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, git.RefNameFromBranch(repo.DefaultBranch)) } + +// isForkPullRequestInput reports whether the run should be treated as a fork pull request. +func isForkPullRequestInput(input *notifyInput) bool { + pr := input.PullRequest + if pr == nil { + return false + } + switch pr.Flow { + case issues_model.PullRequestFlowGithub: + return pr.IsFromFork() + case issues_model.PullRequestFlowAGit: + // There is no fork concept in agit flow, anyone with read permission can push refs/for// to the repo. + // So we can treat it as a fork pull request because it may be from an untrusted user + return true + default: + // unknown flow, assume it's a fork pull request to be safe + return true + } +} + +// detectAndHandleScopedWorkflows detects scoped workflows registered for the consuming repo +func detectAndHandleScopedWorkflows( + ctx context.Context, + input *notifyInput, + ref git.RefName, + consumerGitRepo *git.Repository, + consumerCommit *git.Commit, +) error { + // TODO: support workflow_run and schedule + if input.Event == webhook_module.HookEventWorkflowRun || input.Event == webhook_module.HookEventSchedule { + return nil + } + + sources, err := actions_model.GetEffectiveScopedWorkflowSources(ctx, input.Repo.OwnerID) + if err != nil { + return fmt.Errorf("GetEffectiveScopedWorkflowSources: %w", err) + } + if len(sources) == 0 { + return nil + } + + p, err := json.Marshal(input.Payload) + if err != nil { + return fmt.Errorf("json.Marshal: %w", err) + } + isForkPullRequest := isForkPullRequestInput(input) + actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig() + + // A filtered-out scoped workflow only posts a skipped status when its context is a required check. + requiredGlobs, err := getAllRequiredStatusContextGlobs(ctx, input.Repo) + if err != nil { + log.Error("scoped workflows: required status contexts for %s: %v", input.Repo.FullName(), err) + } + + // The same source repo may be registered at both the owner and instance level; dedup + // the IDs and batch-load them in one query instead of one round-trip per source. + seen := make(container.Set[int64], len(sources)) + for _, source := range sources { + seen.Add(source.SourceRepoID) + } + sourceRepoIDs := seen.Values() + + sourceRepos, err := repo_model.GetRepositoriesMapByIDs(ctx, sourceRepoIDs) + if err != nil { + return fmt.Errorf("GetRepositoriesMapByIDs: %w", err) + } + + for _, sourceRepoID := range sourceRepoIDs { + sourceRepo := sourceRepos[sourceRepoID] + if sourceRepo == nil { + // don't abort the other effective sources for this event + log.Error("scoped workflows: source repo %d for consumer %s not found", sourceRepoID, input.Repo.FullName()) + continue + } + if sourceRepo.IsEmpty { + continue + } + + sourceCommitSHA, detected, filtered, err := detectScopedWorkflowsForSource(ctx, input, consumerGitRepo, consumerCommit, sourceRepo) + if err != nil { + log.Error("scoped workflows: source %d for consumer %s: %v", sourceRepoID, input.Repo.FullName(), err) + continue + } + + for _, dwf := range detected { + // A consuming repo can opt out of a non-required scoped workflow. + // A required workflow (marked required at any effective level) can never be opted out. + if actions_model.ScopedWorkflowOptedOut(actionsConfig, sources, sourceRepo.ID, dwf.EntryName) { + continue + } + + if err := buildApproveAndInsertRun(ctx, input, ref, consumerCommit, string(p), isForkPullRequest, dwf, sourceRepo.ID, sourceCommitSHA, true); err != nil { + log.Error("scoped workflows: source %s workflow %s: %v", sourceRepo.FullName(), dwf.EntryName, err) + continue + } + } + + // A filtered-out scoped workflow posts a skipped commit status for its required-check contexts. + if len(filtered) > 0 && len(requiredGlobs) > 0 { + scopedPrefix := actions_model.ScopedStatusContextPrefix(ctx, sourceRepo.ID) + for _, dwf := range filtered { + if actions_model.ScopedWorkflowOptedOut(actionsConfig, sources, sourceRepo.ID, dwf.EntryName) { + continue + } + if err := CreateSkippedCommitStatusForFilteredWorkflow(ctx, input.Repo, input.Event, dwf.TriggerEvent.Name, dwf.EntryName, dwf.Content, input.Payload, scopedPrefix, requiredGlobs); err != nil { + log.Error("scoped workflows: skipped commit status for source %s workflow %s: %v", sourceRepo.FullName(), dwf.EntryName, err) + continue + } + } + } + } + + return nil +} + +// detectScopedWorkflowsForSource detects the scoped workflows from the source repo at its default branch. +// detected are the workflows to run; filtered matched the event but were excluded by a branch/paths +// filter, and later post a skipped commit status only for a required-check context. +func detectScopedWorkflowsForSource( + ctx context.Context, + input *notifyInput, + consumerGitRepo *git.Repository, + consumerCommit *git.Commit, + sourceRepo *repo_model.Repository, +) (sourceCommitSHA string, detected, filtered []*actions_module.DetectedWorkflow, err error) { + // scoped workflow content is always taken from the source repo's default branch; the parse is cached per (source, default-branch SHA) and reused across consuming repos/events + + sourceCommitSHA, parsed, err := LoadParsedScopedWorkflows(ctx, sourceRepo) + if err != nil { + return "", nil, nil, err + } + detected, filtered = actions_module.MatchScopedWorkflows(ctx, parsed, consumerGitRepo, consumerCommit, input.Event, input.Payload) + return sourceCommitSHA, detected, filtered, nil +} diff --git a/services/actions/rerun.go b/services/actions/rerun.go index 1ef84608a0..6ec2463438 100644 --- a/services/actions/rerun.go +++ b/services/actions/rerun.go @@ -88,7 +88,16 @@ func validateRerun(ctx context.Context, run *actions_model.ActionRun, repo *repo } cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions) cfg := cfgUnit.ActionsConfig() - if cfg.IsWorkflowDisabled(run.WorkflowID) { + if run.IsScopedRun { + // a required scoped workflow can never be opted out, so a stale disabled flag must not block rerun + optedOut, err := actions_model.IsScopedWorkflowOptedOut(ctx, cfg, repo.OwnerID, run.WorkflowRepoID, run.WorkflowID) + if err != nil { + return err + } + if optedOut { + return util.NewInvalidArgumentErrorf("scoped workflow %s is disabled", run.WorkflowID) + } + } else if cfg.IsWorkflowDisabled(run.WorkflowID) { return util.NewInvalidArgumentErrorf("workflow %s is disabled", run.WorkflowID) } @@ -506,6 +515,7 @@ func cloneRunJobForAttempt(templateJob *actions_model.ActionRunJob, attempt *act AttemptJobID: templateJob.AttemptJobID, Needs: slices.Clone(templateJob.Needs), RunsOn: slices.Clone(templateJob.RunsOn), + ContinueOnError: templateJob.ContinueOnError, Status: templateJob.Status, RawConcurrency: templateJob.RawConcurrency, IsConcurrencyEvaluated: templateJob.IsConcurrencyEvaluated, diff --git a/services/actions/rerun_test.go b/services/actions/rerun_test.go index baa678a440..9c0f63e165 100644 --- a/services/actions/rerun_test.go +++ b/services/actions/rerun_test.go @@ -80,6 +80,22 @@ func TestGetFailedJobsForRerun(t *testing.T) { }) } +func TestCloneRunJobForAttempt(t *testing.T) { + attempt := &actions_model.ActionRunAttempt{ID: 42, Attempt: 2} + + t.Run("preserves continue-on-error", func(t *testing.T) { + template := &actions_model.ActionRunJob{ContinueOnError: true, Status: actions_model.StatusFailure} + clone := cloneRunJobForAttempt(template, attempt) + assert.True(t, clone.ContinueOnError) + }) + + t.Run("defaults to false when template has it unset", func(t *testing.T) { + template := &actions_model.ActionRunJob{ContinueOnError: false} + clone := cloneRunJobForAttempt(template, attempt) + assert.False(t, clone.ContinueOnError) + }) +} + func TestRerunValidation(t *testing.T) { runningRun := &actions_model.ActionRun{Status: actions_model.StatusRunning} diff --git a/services/actions/reusable_workflow.go b/services/actions/reusable_workflow.go index 65a6acfbd0..119c11cba4 100644 --- a/services/actions/reusable_workflow.go +++ b/services/actions/reusable_workflow.go @@ -13,6 +13,7 @@ import ( perm_model "gitea.dev/models/perm" access_model "gitea.dev/models/perm/access" repo_model "gitea.dev/models/repo" + actions_module "gitea.dev/modules/actions" "gitea.dev/modules/actions/jobparser" "gitea.dev/modules/container" "gitea.dev/modules/gitrepo" @@ -60,6 +61,11 @@ func loadReusableWorkflowSource(ctx context.Context, run *actions_model.ActionRu return nil, 0, "", err } if !ok { + if run.IsScopedRun { + // A scoped workflow's cross-repo "uses:" is resolved with the consuming repo's read permission, + // so the referenced repo must be readable by every consumer. Make that explicit in the failure. + return nil, 0, "", fmt.Errorf("no permission to read reusable workflow %s/%s: a scoped workflow's cross-repo \"uses:\" is resolved with the consuming repository %q read permission", ref.Owner, ref.Repo, run.Repo.FullName()) + } return nil, 0, "", fmt.Errorf("no permission to read reusable workflow from %s/%s", ref.Owner, ref.Repo) } bytes, resolvedSHA, err := readWorkflowFromRepo(ctx, repo, ref.Ref, ref.Path) @@ -73,17 +79,17 @@ func loadReusableWorkflowSource(ctx context.Context, run *actions_model.ActionRu // readWorkflowFromRepo loads a workflow file from `repo` at `refOrSHA` and returns its content plus the resolved commit SHA. func readWorkflowFromRepo(ctx context.Context, repo *repo_model.Repository, refOrSHA, path string) ([]byte, string, error) { - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + gitRepo, err := gitrepo.OpenRepository(repo) if err != nil { return nil, "", fmt.Errorf("open repo %s: %w", repo.FullName(), err) } defer gitRepo.Close() - commit, err := gitRepo.GetCommit(refOrSHA) + commit, err := gitRepo.GetCommit(ctx, refOrSHA) if err != nil { return nil, "", fmt.Errorf("get commit %q in %s: %w", refOrSHA, repo.FullName(), err) } - str, err := commit.GetFileContent(path, 1024*1024) + str, err := commit.GetFileContent(ctx, gitRepo, path, 1024*1024) if err != nil { return nil, "", fmt.Errorf("read %s@%s:%s: %w", repo.FullName(), refOrSHA, path, err) } @@ -325,6 +331,7 @@ func insertCallerChildren(ctx context.Context, run *actions_model.ActionRun, att AttemptJobID: attemptJobID, Needs: needs, RunsOn: parsedChild.RunsOn(), + ContinueOnError: parsedChild.GetContinueOnError(), Status: actions_model.StatusBlocked, ParentJobID: caller.ID, WorkflowSourceRepoID: sourceRepoID, @@ -358,5 +365,13 @@ func ResolveUses(ctx context.Context, uses string) (*jobparser.UsesRef, error) { // RoutePath is the instance-relative path (AppSubURL already stripped), e.g. "/owner/repo/.gitea/workflows/file.yml@ref". uses = strings.TrimPrefix(gsu.RoutePath, "/") } - return jobparser.ParseUses(uses) + ref, err := jobparser.ParseUses(uses) + if err != nil { + return nil, err + } + // jobparser only validates syntax; enforce the (instance-configurable) directory allowlist here. + if !actions_module.IsWorkflowOrScopedWorkflow(ref.Path) { + return nil, fmt.Errorf(`"uses:" path %q must be under a configured workflow directory (WORKFLOW_DIRS or SCOPED_WORKFLOW_DIRS)`, ref.Path) + } + return ref, nil } diff --git a/services/actions/reusable_workflow_test.go b/services/actions/reusable_workflow_test.go index a7bb41ba8a..28b6696c36 100644 --- a/services/actions/reusable_workflow_test.go +++ b/services/actions/reusable_workflow_test.go @@ -139,6 +139,8 @@ func buildCallerChain(t *testing.T, callerUses ...string) []*actions_model.Actio func TestResolveUses(t *testing.T) { defer test.MockVariableValue(&setting.AppURL, "https://gitea.example.com/sub/")() defer test.MockVariableValue(&setting.AppSubURL, "/sub")() + defer test.MockVariableValue(&setting.Actions.WorkflowDirs, []string{".gitea/workflows", ".github/workflows"})() + defer test.MockVariableValue(&setting.Actions.ScopedWorkflowDirs, []string{".gitea/scoped_workflows"})() ctx := t.Context() t.Run("LocalForms", func(t *testing.T) { @@ -152,6 +154,34 @@ func TestResolveUses(t *testing.T) { assert.Equal(t, jobparser.UsesRef{Kind: jobparser.UsesKindLocalCrossRepo, Owner: "owner", Repo: "repo", Path: ".gitea/workflows/build.yml", Ref: "v1"}, *ref) }) + t.Run("DirectoryAllowlist", func(t *testing.T) { + // SCOPED_WORKFLOW_DIRS is allowed (local and cross-repo). + ref, err := ResolveUses(ctx, "./.gitea/scoped_workflows/lib.yml") + require.NoError(t, err) + assert.Equal(t, ".gitea/scoped_workflows/lib.yml", ref.Path) + + ref, err = ResolveUses(ctx, "owner/repo/.gitea/scoped_workflows/lib.yml@v1") + require.NoError(t, err) + assert.Equal(t, ".gitea/scoped_workflows/lib.yml", ref.Path) + + // A directory that is neither WORKFLOW_DIRS nor SCOPED_WORKFLOW_DIRS parses but is rejected by the allowlist. + _, err = ResolveUses(ctx, "./not-workflows/build.yml") + require.Error(t, err) + _, err = ResolveUses(ctx, "owner/repo/lib/build.yml@v1") + require.Error(t, err) + }) + + t.Run("ConfigurableWorkflowDirs", func(t *testing.T) { + // A non-default WORKFLOW_DIRS is honored (the hardcoded ".gitea/workflows" is no longer special). + defer test.MockVariableValue(&setting.Actions.WorkflowDirs, []string{".gitea/ci"})() + ref, err := ResolveUses(ctx, "./.gitea/ci/build.yml") + require.NoError(t, err) + assert.Equal(t, ".gitea/ci/build.yml", ref.Path) + + _, err = ResolveUses(ctx, "./.gitea/workflows/build.yml") // no longer a configured dir + require.Error(t, err) + }) + t.Run("LocalInstanceURL", func(t *testing.T) { // An absolute URL on this instance (incl. AppSubURL) resolves to the equivalent cross-repo ref. ref, err := ResolveUses(ctx, "https://gitea.example.com/sub/owner/repo/.gitea/workflows/ci.yml@refs/heads/main") diff --git a/services/actions/run.go b/services/actions/run.go index 82335af861..fc2631e19d 100644 --- a/services/actions/run.go +++ b/services/actions/run.go @@ -21,6 +21,10 @@ import ( // It parses the workflow content, evaluates concurrency if needed, and inserts the run and its jobs into the database. // The title will be cut off at 255 characters if it's longer than 255 characters. func PrepareRunAndInsert(ctx context.Context, content []byte, run *actions_model.ActionRun, inputsWithDefaults map[string]any) error { + if run.WorkflowRepoID == 0 { + return fmt.Errorf("WorkflowRepoID must be set before insert (repo %d, workflow %q)", run.RepoID, run.WorkflowID) + } + if err := run.LoadAttributes(ctx); err != nil { return fmt.Errorf("LoadAttributes: %w", err) } @@ -162,8 +166,9 @@ func InsertRun(ctx context.Context, run *actions_model.ActionRun, content []byte Needs: needs, RunsOn: job.RunsOn(), Status: util.Iif(shouldBlockJob, actions_model.StatusBlocked, actions_model.StatusWaiting), - WorkflowSourceRepoID: run.RepoID, - WorkflowSourceCommitSHA: run.CommitSHA, + WorkflowSourceRepoID: run.WorkflowRepoID, + WorkflowSourceCommitSHA: run.WorkflowCommitSHA, + ContinueOnError: job.GetContinueOnError(), } // Parse workflow/job permissions (no clamping here) if perms := ExtractJobPermissionsFromWorkflow(v, job); perms != nil { diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index 5995d003be..0decf7b968 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -6,16 +6,22 @@ package actions import ( "context" "fmt" + "maps" "time" actions_model "gitea.dev/models/actions" "gitea.dev/models/db" + "gitea.dev/models/organization" + perm_model "gitea.dev/models/perm" + access_model "gitea.dev/models/perm/access" repo_model "gitea.dev/models/repo" "gitea.dev/models/unit" + user_model "gitea.dev/models/user" "gitea.dev/modules/json" "gitea.dev/modules/log" "gitea.dev/modules/timeutil" webhook_module "gitea.dev/modules/webhook" + "gitea.dev/services/convert" ) // StartScheduleTasks start the task @@ -102,7 +108,19 @@ func startTasks(ctx context.Context) error { // It creates an action run based on the schedule, inserts it into the database, and creates commit statuses for each job. func CreateScheduleTask(ctx context.Context, spec *actions_model.ActionScheduleSpec) error { cron := spec.Schedule - eventPayload := withScheduleInEventPayload(cron.EventPayload, spec.Spec) + + // Scheduled runs carry no webhook payload; synthesize what github.event.* expects. + if err := spec.Repo.LoadOwner(ctx); err != nil { + return fmt.Errorf("LoadOwner: %w", err) + } + fields := map[string]any{ + "repository": convert.ToRepo(ctx, spec.Repo, access_model.Permission{AccessMode: perm_model.AccessModeRead}), + "sender": convert.ToUser(ctx, user_model.NewActionsUser(), nil), + } + if spec.Repo.Owner.IsOrganization() { + fields["organization"] = convert.ToOrganization(ctx, organization.OrgFromUser(spec.Repo.Owner)) + } + eventPayload := withScheduleInEventPayload(cron.EventPayload, spec.Spec, fields) // Create a new action run based on the schedule run := &actions_model.ActionRun{ @@ -118,6 +136,9 @@ func CreateScheduleTask(ctx context.Context, spec *actions_model.ActionScheduleS TriggerEvent: string(webhook_module.HookEventSchedule), ScheduleID: cron.ID, Status: actions_model.StatusWaiting, + // schedule runs the repo's own workflow at the recorded commit + WorkflowRepoID: cron.RepoID, + WorkflowCommitSHA: cron.CommitSHA, } // FIXME cron.Content might be outdated if the workflow file has been changed. @@ -131,7 +152,7 @@ func CreateScheduleTask(ctx context.Context, spec *actions_model.ActionScheduleS return nil } -func withScheduleInEventPayload(eventPayload, schedule string) string { +func withScheduleInEventPayload(eventPayload, schedule string, fields map[string]any) string { if schedule == "" { return eventPayload } @@ -150,6 +171,7 @@ func withScheduleInEventPayload(eventPayload, schedule string) string { event = map[string]any{} } + maps.Copy(event, fields) event["schedule"] = schedule updatedPayload, err := json.Marshal(event) if err != nil { diff --git a/services/actions/schedule_tasks_test.go b/services/actions/schedule_tasks_test.go index ec68eb54fb..8514625486 100644 --- a/services/actions/schedule_tasks_test.go +++ b/services/actions/schedule_tasks_test.go @@ -7,6 +7,7 @@ import ( "testing" "gitea.dev/modules/json" + api "gitea.dev/modules/structs" "github.com/stretchr/testify/assert" ) @@ -14,7 +15,7 @@ import ( func TestWithScheduleInEventPayload(t *testing.T) { t.Run("adds schedule to existing payload", func(t *testing.T) { payload := `{"ref":"refs/heads/main"}` - updated := withScheduleInEventPayload(payload, "*/5 * * * *") + updated := withScheduleInEventPayload(payload, "*/5 * * * *", nil) event := map[string]any{} assert.NoError(t, json.Unmarshal([]byte(updated), &event)) @@ -23,7 +24,7 @@ func TestWithScheduleInEventPayload(t *testing.T) { }) t.Run("adds schedule to null payload", func(t *testing.T) { - updated := withScheduleInEventPayload("null", "37 12 5 1 2") + updated := withScheduleInEventPayload("null", "37 12 5 1 2", nil) event := map[string]any{} assert.NoError(t, json.Unmarshal([]byte(updated), &event)) @@ -31,22 +32,37 @@ func TestWithScheduleInEventPayload(t *testing.T) { }) t.Run("adds schedule to empty payload", func(t *testing.T) { - updated := withScheduleInEventPayload("", "37 12 5 1 2") + updated := withScheduleInEventPayload("", "37 12 5 1 2", nil) event := map[string]any{} assert.NoError(t, json.Unmarshal([]byte(updated), &event)) assert.Equal(t, "37 12 5 1 2", event["schedule"]) }) + t.Run("adds schedule with repository, sender, organization", func(t *testing.T) { + updated := withScheduleInEventPayload("null", "@weekly", map[string]any{ + "repository": &api.Repository{Name: "test-repo"}, + "sender": &api.User{UserName: "test-user"}, + "organization": &api.Organization{Name: "test-org"}, + }) + + event := map[string]any{} + assert.NoError(t, json.Unmarshal([]byte(updated), &event)) + assert.Equal(t, "@weekly", event["schedule"]) + assert.Equal(t, "test-repo", event["repository"].(map[string]any)["name"]) + assert.Equal(t, "test-user", event["sender"].(map[string]any)["login"]) + assert.Equal(t, "test-org", event["organization"].(map[string]any)["name"]) + }) + t.Run("keeps payload when schedule empty", func(t *testing.T) { payload := `{"ref":"refs/heads/main"}` - updated := withScheduleInEventPayload(payload, "") + updated := withScheduleInEventPayload(payload, "", nil) assert.Equal(t, payload, updated) }) t.Run("keeps payload when malformed JSON", func(t *testing.T) { payload := `not a json object` - updated := withScheduleInEventPayload(payload, "*/5 * * * *") + updated := withScheduleInEventPayload(payload, "*/5 * * * *", nil) assert.Equal(t, payload, updated) }) } diff --git a/services/actions/scoped_workflow_cache.go b/services/actions/scoped_workflow_cache.go new file mode 100644 index 0000000000..0288dbf2bf --- /dev/null +++ b/services/actions/scoped_workflow_cache.go @@ -0,0 +1,84 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "fmt" + + git_model "gitea.dev/models/git" + repo_model "gitea.dev/models/repo" + actions_module "gitea.dev/modules/actions" + "gitea.dev/modules/gitrepo" + "gitea.dev/modules/log" + + lru "github.com/hashicorp/golang-lru/v2" +) + +// cachedScopedWorkflows is one source repo's parsed scoped workflows together with the default-branch SHA they were parsed at. +type cachedScopedWorkflows struct { + sha string + parsed []*actions_module.ParsedScopedWorkflow +} + +// scopedWorkflowCache caches each scoped-workflow source repo's parsed workflows, keyed by source repo ID. +// There is exactly one entry per source: a default-branch update is detected by SHA mismatch and overwrites the entry, so stale parses never accumulate. +var scopedWorkflowCache *lru.Cache[int64, *cachedScopedWorkflows] + +const defaultScopedWorkflowCacheSize = 1024 + +func init() { + c, err := lru.New[int64, *cachedScopedWorkflows](defaultScopedWorkflowCacheSize) + if err != nil { + log.Fatal("failed to new scopedWorkflowCache, err: %v", err) + } + scopedWorkflowCache = c +} + +// LoadParsedScopedWorkflows returns the source repo's parsed scoped workflows at its current default-branch HEAD. +func LoadParsedScopedWorkflows(ctx context.Context, sourceRepo *repo_model.Repository) (sha string, parsed []*actions_module.ParsedScopedWorkflow, err error) { + branch, err := git_model.GetBranch(ctx, sourceRepo.ID, sourceRepo.DefaultBranch) + if err != nil { + return "", nil, fmt.Errorf("get source default branch: %w", err) + } + sha = branch.CommitID + + if v, ok := scopedWorkflowCache.Get(sourceRepo.ID); ok && v.sha == sha { + // cache hit at the current default-branch HEAD + return sha, v.parsed, nil + } + + // cache miss: open the source repo at the exact SHA we keyed on + sourceGitRepo, err := gitrepo.OpenRepository(sourceRepo) + if err != nil { + return "", nil, fmt.Errorf("open source repo: %w", err) + } + defer sourceGitRepo.Close() + + sourceCommit, err := sourceGitRepo.GetCommit(ctx, sha) + if err != nil { + return "", nil, fmt.Errorf("get source commit %s: %w", sha, err) + } + parsed, err = actions_module.ParseScopedWorkflows(ctx, sourceGitRepo, sourceCommit) + if err != nil { + return "", nil, err + } + // overwrite this source's single entry (a stale entry from a previous HEAD is replaced, not accumulated) + scopedWorkflowCache.Add(sourceRepo.ID, &cachedScopedWorkflows{sha: sha, parsed: parsed}) + return sha, parsed, nil +} + +// ScopedWorkflowContent returns one scoped workflow's raw content (by entry name) at the source repo's current default-branch HEAD, or nil if no such workflow exists there. +func ScopedWorkflowContent(ctx context.Context, sourceRepo *repo_model.Repository, entryName string) ([]byte, error) { + _, parsed, err := LoadParsedScopedWorkflows(ctx, sourceRepo) + if err != nil { + return nil, err + } + for _, p := range parsed { + if p.EntryName == entryName { + return p.Content, nil + } + } + return nil, nil +} diff --git a/services/actions/task.go b/services/actions/task.go index ff54281cba..e615e4e4bf 100644 --- a/services/actions/task.go +++ b/services/actions/task.go @@ -7,15 +7,60 @@ import ( "context" "errors" "fmt" + "sync" + "time" runnerv1 "gitea.dev/actions-proto-go/runner/v1" actions_model "gitea.dev/models/actions" "gitea.dev/models/db" secret_model "gitea.dev/models/secret" + "gitea.dev/modules/graceful" + "gitea.dev/modules/log" + "gitea.dev/modules/setting" "google.golang.org/protobuf/types/known/structpb" ) +var ( + taskPickSem chan struct{} + taskPickSemOnce sync.Once +) + +func taskPickLimiter() chan struct{} { + taskPickSemOnce.Do(func() { + taskPickSem = make(chan struct{}, setting.Actions.MaxConcurrentTaskPicks) + }) + return taskPickSem +} + +// TryPickTask attempts to assign a task to the runner, bounding the number of +// concurrent assignment transactions to avoid a thundering herd when many +// runners poll at once. When the concurrency limit is reached it returns +// throttled=true without touching the DB, so the caller can let the runner +// retry on its next poll instead of advancing its tasks version. +func TryPickTask(ctx context.Context, runner *actions_model.ActionRunner) (task *runnerv1.Task, ok, throttled bool, err error) { + sem := taskPickLimiter() + select { + case sem <- struct{}{}: + defer func() { <-sem }() + default: + return nil, false, true, nil + } + task, ok, err = PickTask(ctx, runner) + return task, ok, false, err +} + +// releaseTaskForRunnerCleanup releases a claimed task using a fresh, bounded context. The request context +// is typically already canceled when we reach the release paths below, and a DB transaction on a canceled +// context fails immediately, which would strand the claimed job in running state. +func releaseTaskForRunnerCleanup(t *actions_model.ActionTask) { + ctx, cancel := context.WithTimeout(graceful.GetManager().ShutdownContext(), 10*time.Second) + defer cancel() + if relErr := actions_model.ReleaseTaskForRunner(ctx, t); relErr != nil { + log.Error("ReleaseTaskForRunner [task_id: %d]: %v", t.ID, relErr) + } +} + func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv1.Task, bool, error) { var ( task *runnerv1.Task @@ -47,59 +92,24 @@ func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv } } - if err := db.WithTx(ctx, func(ctx context.Context) error { - t, ok, err := actions_model.CreateTaskForRunner(ctx, runner) - if err != nil { - return fmt.Errorf("CreateTaskForRunner: %w", err) - } - if !ok { - return nil - } - - if err := t.LoadAttributes(ctx); err != nil { - return fmt.Errorf("task LoadAttributes: %w", err) - } - job = t.Job - actionTask = t - - secrets, err := secret_model.GetSecretsOfTask(ctx, t) - if err != nil { - return fmt.Errorf("GetSecretsOfTask: %w", err) - } - - vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run) - if err != nil { - return fmt.Errorf("GetVariablesOfRun: %w", err) - } - - needs, err := findTaskNeeds(ctx, job) - if err != nil { - return fmt.Errorf("findTaskNeeds: %w", err) - } - - taskContext, err := generateTaskContext(ctx, t) - if err != nil { - return fmt.Errorf("generateTaskContext: %w", err) - } - - task = &runnerv1.Task{ - Id: t.ID, - WorkflowPayload: t.Job.WorkflowPayload, - Context: taskContext, - Secrets: secrets, - Vars: vars, - Needs: needs, - } - - return nil - }); err != nil { - return nil, false, err + t, ok, err := actions_model.CreateTaskForRunner(ctx, runner) + if err != nil { + return nil, false, fmt.Errorf("CreateTaskForRunner: %w", err) } - - if task == nil { + if !ok { return nil, false, nil } + task, job, err = buildRunnerTask(ctx, t) + if err != nil { + // The job was already claimed but assembling its payload failed; release the + // claim so the job returns to the waiting queue instead of being stranded in + // running state with no runner ever executing it. + releaseTaskForRunnerCleanup(t) + return nil, false, err + } + actionTask = t + CreateCommitStatusForRunJobs(ctx, job.Run, job) NotifyWorkflowJobStatusUpdateWithTask(ctx, job, actionTask) // job.Run is loaded inside the transaction before UpdateRunJob sets run.Started, @@ -108,9 +118,54 @@ func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv NotifyWorkflowRunStatusUpdateWithReload(ctx, job.RepoID, job.RunID) } + // The job is claimed and its payload assembled, but if the request context was cancelled meanwhile, response can no longer reach the runner. + // Release the claim so another runner can pick the job up. + if err := ctx.Err(); err != nil { + releaseTaskForRunnerCleanup(t) + return nil, false, err + } + return task, true, nil } +// buildRunnerTask assembles the runner-facing task payload for an already-claimed +// task. All operations are read-only; on error the caller releases the claim. +func buildRunnerTask(ctx context.Context, t *actions_model.ActionTask) (*runnerv1.Task, *actions_model.ActionRunJob, error) { + if err := t.LoadAttributes(ctx); err != nil { + return nil, nil, fmt.Errorf("task LoadAttributes: %w", err) + } + job := t.Job + + secrets, err := secret_model.GetSecretsOfTask(ctx, t) + if err != nil { + return nil, nil, fmt.Errorf("GetSecretsOfTask: %w", err) + } + + vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run) + if err != nil { + return nil, nil, fmt.Errorf("GetVariablesOfRun: %w", err) + } + + needs, err := findTaskNeeds(ctx, job) + if err != nil { + return nil, nil, fmt.Errorf("findTaskNeeds: %w", err) + } + + taskContext, err := generateTaskContext(ctx, t) + if err != nil { + return nil, nil, fmt.Errorf("generateTaskContext: %w", err) + } + + return &runnerv1.Task{ + Id: t.ID, + WorkflowPayload: t.Job.WorkflowPayload, + Context: taskContext, + Secrets: secrets, + Vars: vars, + Needs: needs, + }, job, nil +} + func generateTaskContext(ctx context.Context, t *actions_model.ActionTask) (*structpb.Struct, error) { giteaRuntimeToken, err := CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID) if err != nil { diff --git a/services/actions/task_test.go b/services/actions/task_test.go new file mode 100644 index 0000000000..21f8615623 --- /dev/null +++ b/services/actions/task_test.go @@ -0,0 +1,74 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + actions_model "gitea.dev/models/actions" + "gitea.dev/models/db" + "gitea.dev/models/unittest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTryPickTaskThrottled(t *testing.T) { + sem := taskPickLimiter() + + // Saturate every assignment slot so the next attempt must be throttled. + for range cap(sem) { + sem <- struct{}{} + } + defer func() { + for range cap(sem) { + <-sem + } + }() + + // No DB access happens on the throttled path, so this is safe without fixtures. + task, ok, throttled, err := TryPickTask(t.Context(), &actions_model.ActionRunner{}) + require.NoError(t, err) + assert.Nil(t, task) + assert.False(t, ok) + assert.True(t, throttled) +} + +// TestReleaseTaskForRunnerCleanup verifies the cleanup used by PickTask releases a claimed task through a +// fresh context. PickTask reaches this path when the request context is already canceled, and on +// PostgreSQL/MySQL a DB transaction on a canceled context fails immediately; reusing it would strand the +// claimed job in running state, so the cleanup must not use the request context. +func TestReleaseTaskForRunnerCleanup(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + run := &actions_model.ActionRun{ + Title: "cleanup-run", RepoID: 1, OwnerID: 2, WorkflowID: "test.yaml", + TriggerUserID: 2, Ref: "refs/heads/main", + CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0", Event: "push", TriggerEvent: "push", + Status: actions_model.StatusWaiting, + } + require.NoError(t, db.Insert(t.Context(), run)) + job := &actions_model.ActionRunJob{ + RunID: run.ID, RepoID: run.RepoID, OwnerID: run.OwnerID, CommitSHA: run.CommitSHA, + Name: "cleanup-job", Attempt: 1, JobID: "cleanup-job", Status: actions_model.StatusWaiting, + RunsOn: []string{"ubuntu-latest"}, + WorkflowPayload: []byte("on: push\njobs:\n cleanup-job:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n"), + } + require.NoError(t, db.Insert(t.Context(), job)) + runner := &actions_model.ActionRunner{Name: "cleanup-runner", AgentLabels: []string{"ubuntu-latest"}} + runner.GenerateAndFillToken() + require.NoError(t, db.Insert(t.Context(), runner)) + + task, ok, err := actions_model.CreateTaskForRunner(t.Context(), runner) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, actions_model.StatusRunning, unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: job.ID}).Status) + + // the cleanup helper uses its own context, so the claimed job is returned to the waiting queue + releaseTaskForRunnerCleanup(task) + released := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: job.ID}) + assert.Equal(t, actions_model.StatusWaiting, released.Status) + assert.Zero(t, released.TaskID) + unittest.AssertNotExistsBean(t, &actions_model.ActionTask{ID: task.ID}) +} diff --git a/services/actions/workflow.go b/services/actions/workflow.go index a748b0859c..17fce34ba1 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -43,7 +43,9 @@ func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnabl return repo_model.UpdateRepoUnitConfig(ctx, cfgUnit) } -func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, workflowID, ref string, processInputs func(model *model.WorkflowDispatch, inputs map[string]any) error) (runID int64, _ error) { +// DispatchActionWorkflow manually triggers a workflow_dispatch run. +// scopedWorkflowSourceRepoID selects the workflow source: 0 means a repo-level workflow in this repo; a non-zero value is the source repo of a scoped workflow. +func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, workflowID, ref string, scopedWorkflowSourceRepoID int64, processInputs func(model *model.WorkflowDispatch, inputs map[string]any) error) (runID int64, _ error) { if workflowID == "" { return 0, util.ErrorWrapTranslatable( util.NewNotExistErrorf("workflowID is empty"), @@ -58,10 +60,20 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re ) } - // can not rerun job when workflow is disabled + isScoped := scopedWorkflowSourceRepoID > 0 + cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions) cfg := cfgUnit.ActionsConfig() - if cfg.IsWorkflowDisabled(workflowID) { + var workflowDisabled bool + if isScoped { + var err error + if workflowDisabled, err = actions_model.IsScopedWorkflowOptedOut(ctx, cfg, repo.OwnerID, scopedWorkflowSourceRepoID, workflowID); err != nil { + return 0, err + } + } else { + workflowDisabled = cfg.IsWorkflowDisabled(workflowID) + } + if workflowDisabled { return 0, util.ErrorWrapTranslatable( util.NewPermissionDeniedErrorf("workflow is disabled"), "actions.workflow.disabled", @@ -73,12 +85,12 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re var runTargetCommit *git.Commit var err error if refName.IsTag() { - runTargetCommit, err = gitRepo.GetTagCommit(refName.TagName()) + runTargetCommit, err = gitRepo.GetTagCommit(ctx, refName.TagName()) } else if refName.IsBranch() { - runTargetCommit, err = gitRepo.GetBranchCommit(refName.BranchName()) + runTargetCommit, err = gitRepo.GetBranchCommit(ctx, refName.BranchName()) } else { refName = git.RefNameFromBranch(ref) - runTargetCommit, err = gitRepo.GetBranchCommit(ref) + runTargetCommit, err = gitRepo.GetBranchCommit(ctx, ref) } if err != nil { return 0, util.ErrorWrapTranslatable( @@ -87,15 +99,6 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re ) } - // get workflow entry from runTargetCommit - _, entries, err := actions.ListWorkflows(runTargetCommit) - if err != nil { - return 0, err - } - - // find workflow from commit - var entry *git.TreeEntry - run := &actions_model.ActionRun{ Title: runTargetCommit.MessageTitle(), RepoID: repo.ID, @@ -110,24 +113,13 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re Event: "workflow_dispatch", TriggerEvent: "workflow_dispatch", Status: actions_model.StatusWaiting, + // local dispatch: own repo at the target commit; the scoped path overrides these below + WorkflowRepoID: repo.ID, + WorkflowCommitSHA: runTargetCommit.ID.String(), } - for _, e := range entries { - if e.Name() != workflowID { - continue - } - entry = e - break - } - - if entry == nil { - return 0, util.ErrorWrapTranslatable( - util.NewNotExistErrorf("workflow %q doesn't exist", workflowID), - "actions.workflow.not_found", workflowID, - ) - } - - content, err := actions.GetContentFromEntry(entry) + // resolve the workflow content and record its source on the run (scoped runs read from the source repo) + content, err := resolveDispatchWorkflowContent(ctx, repo, gitRepo, runTargetCommit, workflowID, scopedWorkflowSourceRepoID, isScoped, run) if err != nil { return 0, err } @@ -152,6 +144,11 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re if err = processInputs(workflowDispatch, inputsWithDefaults); err != nil { return 0, err } + // The dispatch callbacks fill boolean inputs as the strings "true"/"false". Normalize them to + // native JSON booleans so `type: boolean` inputs match GitHub, whose `inputs` context preserves + // booleans as booleans. Without this, a server-side needs-gated job `if: inputs.flag == true` + // evaluates against the string "true" and never matches, leaving the job blocked forever. + coerceDispatchInputTypes(workflowDispatch, inputsWithDefaults) // ctx.Req.PostForm -> WorkflowDispatchPayload.Inputs -> ActionRun.EventPayload -> runner: ghc.Event // https://docs.github.com/en/actions/learn-github-actions/contexts#github-context @@ -176,3 +173,79 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re } return run.ID, nil } + +// coerceDispatchInputTypes normalizes workflow_dispatch input values to the JSON types declared by +// the workflow. Only booleans are coerced, matching GitHub, whose `inputs` context "preserves +// Boolean values as Booleans instead of converting them to strings" while every other type stays a +// string. workflow_dispatch has no `number` type (its input types are string, choice, boolean and +// environment), so booleans are the complete set to coerce here. +// A value that is already a bool is left untouched, so the coercion is idempotent. +func coerceDispatchInputTypes(dispatch *model.WorkflowDispatch, inputs map[string]any) { + for name, cfg := range dispatch.Inputs { + if cfg.Type != "boolean" { + continue + } + if s, ok := inputs[name].(string); ok { + inputs[name] = s == "true" + } + } +} + +// resolveDispatchWorkflowContent returns the YAML for a dispatched workflow and records its source on the run. +// - Repo-level: from the consumer's runTargetCommit. +// - Scoped: from the source repo's default branch. +func resolveDispatchWorkflowContent(ctx reqctx.RequestContext, repo *repo_model.Repository, gitRepo *git.Repository, runTargetCommit *git.Commit, workflowID string, sourceRepoID int64, isScoped bool, run *actions_model.ActionRun) ([]byte, error) { + if isScoped { + return resolveScopedDispatchContent(ctx, repo, sourceRepoID, workflowID, run) + } + + _, entries, err := actions.ListWorkflows(ctx, gitRepo, runTargetCommit) + if err != nil { + return nil, err + } + for _, e := range entries { + if e.Name() == workflowID { + return actions.GetContentFromEntry(ctx, gitRepo, e) + } + } + return nil, util.ErrorWrapTranslatable( + util.NewNotExistErrorf("workflow %q doesn't exist", workflowID), + "actions.workflow.not_found", workflowID, + ) +} + +func resolveScopedDispatchContent(ctx reqctx.RequestContext, repo *repo_model.Repository, sourceRepoID int64, workflowID string, run *actions_model.ActionRun) ([]byte, error) { + // the source must be an effective scoped source for this consumer repo + effective, err := actions_model.IsScopedWorkflowSourceEffective(ctx, repo.OwnerID, sourceRepoID) + if err != nil { + return nil, err + } + if !effective { + return nil, util.ErrorWrapTranslatable( + util.NewNotExistErrorf("scoped workflow source %d is not effective for this repository", sourceRepoID), + "actions.workflow.not_found", workflowID, + ) + } + + sourceRepo, err := repo_model.GetRepositoryByID(ctx, sourceRepoID) + if err != nil { + return nil, err + } + + sha, parsed, err := LoadParsedScopedWorkflows(ctx, sourceRepo) + if err != nil { + return nil, err + } + for _, p := range parsed { + if p.EntryName == workflowID { + run.WorkflowRepoID = sourceRepo.ID + run.WorkflowCommitSHA = sha + run.IsScopedRun = true + return p.Content, nil + } + } + return nil, util.ErrorWrapTranslatable( + util.NewNotExistErrorf("scoped workflow %q doesn't exist", workflowID), + "actions.workflow.not_found", workflowID, + ) +} diff --git a/services/actions/workflow_test.go b/services/actions/workflow_test.go new file mode 100644 index 0000000000..62f760b8db --- /dev/null +++ b/services/actions/workflow_test.go @@ -0,0 +1,41 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + "gitea.com/gitea/runner/act/model" + "github.com/stretchr/testify/assert" +) + +func TestCoerceDispatchInputTypes(t *testing.T) { + dispatch := &model.WorkflowDispatch{ + Inputs: map[string]model.WorkflowDispatchInput{ + "build_server": {Type: "boolean"}, + "dry_run": {Type: "boolean"}, + "already_bool": {Type: "boolean"}, + "version": {Type: "string"}, + }, + } + + inputs := map[string]any{ + // dispatch callbacks fill booleans as strconv.FormatBool(...) strings + "build_server": "true", + "dry_run": "false", + // already-native booleans are passed through unchanged (coercion is idempotent) + "already_bool": true, + // non-boolean inputs must be left untouched + "version": "1.2.3", + } + + coerceDispatchInputTypes(dispatch, inputs) + + // Regression: without coercion these stay strings, and a server-side needs-gated + // job `if: inputs.build_server == true` never matches, leaving the job blocked. + assert.Equal(t, true, inputs["build_server"]) + assert.Equal(t, false, inputs["dry_run"]) + assert.Equal(t, true, inputs["already_bool"]) + assert.Equal(t, "1.2.3", inputs["version"]) +} diff --git a/services/agit/agit.go b/services/agit/agit.go index bccd89b0fe..2624312b9a 100644 --- a/services/agit/agit.go +++ b/services/agit/agit.go @@ -146,7 +146,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. var commit *git.Commit if title == "" || description == "" { - commit, err = gitRepo.GetCommit(opts.NewCommitIDs[i]) + commit, err = gitRepo.GetCommit(ctx, opts.NewCommitIDs[i]) if err != nil { return nil, fmt.Errorf("failed to get commit %s in repository: %s Error: %w", opts.NewCommitIDs[i], repo.FullName(), err) } @@ -209,7 +209,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. return nil, fmt.Errorf("unable to load base repository for PR[%d] Error: %w", pr.ID, err) } - oldCommitID, err := gitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + oldCommitID, err := gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) if err != nil { return nil, fmt.Errorf("unable to get ref commit id in base repository for PR[%d] Error: %w", pr.ID, err) } diff --git a/services/asymkey/commit_test.go b/services/asymkey/commit_test.go index e0ab47573b..03eac38aa7 100644 --- a/services/asymkey/commit_test.go +++ b/services/asymkey/commit_test.go @@ -37,7 +37,7 @@ func TestParseCommitWithSSHSignature(t *testing.T) { require.NoError(t, err) t.Run("UserSSHKey", func(t *testing.T) { - commit, err := git.CommitFromReader(nil, git.Sha1ObjectFormat.EmptyObjectID(), strings.NewReader(`tree a3b1fad553e0f9a2b4a58327bebde36c7da75aa2 + commit, err := git.CommitFromReader(git.Sha1ObjectFormat.EmptyObjectID(), strings.NewReader(`tree a3b1fad553e0f9a2b4a58327bebde36c7da75aa2 author user2 1752194028 -0700 committer user2 1752194028 -0700 gpgsig -----BEGIN SSH SIGNATURE----- @@ -68,7 +68,7 @@ init project defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "gitea@fake.local")() defer test.MockVariableValue(&setting.Repository.Signing.TrustedSSHKeys, []string{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH6Y4idVaW3E+bLw1uqoAfJD7o5Siu+HqS51E9oQLPE9"})() - commit, err := git.CommitFromReader(nil, git.Sha1ObjectFormat.EmptyObjectID(), strings.NewReader(`tree 9a93ffa76e8b72bdb6431910b3a506fa2b39f42e + commit, err := git.CommitFromReader(git.Sha1ObjectFormat.EmptyObjectID(), strings.NewReader(`tree 9a93ffa76e8b72bdb6431910b3a506fa2b39f42e author User Two 1749230009 +0200 committer User Two 1749230009 +0200 gpgsig -----BEGIN SSH SIGNATURE----- diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go index 8ff62f60c8..b7c47e3ab5 100644 --- a/services/asymkey/sign.go +++ b/services/asymkey/sign.go @@ -17,7 +17,6 @@ import ( repo_model "gitea.dev/models/repo" user_model "gitea.dev/models/user" "gitea.dev/modules/git" - "gitea.dev/modules/gitrepo" "gitea.dev/modules/log" "gitea.dev/modules/process" "gitea.dev/modules/setting" @@ -171,7 +170,7 @@ Loop: // SignWikiCommit determines if we should sign the commits to this repository wiki func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) { rules := signingModeFromStrings(setting.Repository.Signing.Wiki) - signingKey, sig := gitrepo.GetSigningKey(ctx) + signingKey, sig := git.GetSigningKey(ctx) if signingKey == nil { return false, nil, nil, &ErrWontSign{noKey} } @@ -200,7 +199,7 @@ Loop: return false, nil, nil, &ErrWontSign{twofa} } case parentSigned: - commit, err := gitRepo.GetCommit("HEAD") + commit, err := gitRepo.GetCommit(ctx, "HEAD") if err != nil { return false, nil, nil, err } @@ -248,12 +247,12 @@ Loop: return false, nil, nil, &ErrWontSign{twofa} } case parentSigned: - isEmpty, err := gitRepo.IsEmpty() + isEmpty, err := gitRepo.IsEmpty(ctx) if err != nil { return false, nil, nil, err } if !isEmpty { - commit, err := gitRepo.GetCommit(parentCommit) + commit, err := gitRepo.GetCommit(ctx, parentCommit) if err != nil { return false, nil, nil, err } @@ -270,24 +269,26 @@ Loop: return true, signingKey, sig, nil } -// SignMerge determines if we should sign a PR merge commit to the base repository -func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, gitRepo *git.Repository) (bool, *git.SigningKey, *git.Signature, error) { +// SignMerge determines if we should sign a PR merge commit to the base repository. +// baseRef and headRef must resolve in gitRepo. Callers pass the temporary merge repo's own +// refs for an update by merge, whose fake reverse PR has no head ref in the base repository. +func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, gitRepo *git.Repository, baseRef, headRef string) (bool, *git.SigningKey, *git.Signature, error) { if err := pr.LoadBaseRepo(ctx); err != nil { log.Error("Unable to get Base Repo for pull request") return false, nil, nil, err } repo := pr.BaseRepo - baseCommit, err := gitRepo.GetCommit(pr.BaseBranch) + baseCommit, err := gitRepo.GetCommit(ctx, baseRef) if err != nil { return false, nil, nil, err } - headCommit, err := gitRepo.GetCommit(pr.GetGitHeadRefName()) + headCommit, err := gitRepo.GetCommit(ctx, headRef) if err != nil { return false, nil, nil, err } - signingKey, signer := gitrepo.GetSigningKey(ctx) + signingKey, signer := git.GetSigningKey(ctx) if signingKey == nil { return false, nil, nil, &ErrWontSign{noKey} } @@ -338,7 +339,7 @@ Loop: return false, nil, nil, &ErrWontSign{headSigned} } case commitsSigned: - verified, err := AllHeadCommitsVerified(ctx, pr, gitRepo) + verified, err := allCommitsVerified(ctx, gitRepo, baseCommit, headCommit) if err != nil { return false, nil, nil, err } @@ -353,19 +354,21 @@ Loop: // AllHeadCommitsVerified checks that every new commit in the PR head has a // verified signature. func AllHeadCommitsVerified(ctx context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) (bool, error) { - baseCommit, err := gitRepo.GetCommit(pr.BaseBranch) + baseCommit, err := gitRepo.GetCommit(ctx, pr.BaseBranch) if err != nil { return false, err } - headCommit, err := gitRepo.GetCommit(pr.GetGitHeadRefName()) + headCommit, err := gitRepo.GetCommit(ctx, pr.GetGitHeadRefName()) if err != nil { return false, err } - mergeBaseCommit, err := gitrepo.MergeBase(ctx, pr.BaseRepo, baseCommit.ID.String(), headCommit.ID.String()) - if err != nil { - return false, err - } - commitList, err := headCommit.CommitsBeforeUntil(git.RefNameFromCommit(mergeBaseCommit)) + return allCommitsVerified(ctx, gitRepo, baseCommit, headCommit) +} + +// allCommitsVerified checks the commits a merge would introduce, those reachable from +// headCommit but not from baseCommit. Both commits must come from gitRepo. +func allCommitsVerified(ctx context.Context, gitRepo *git.Repository, baseCommit, headCommit *git.Commit) (bool, error) { + commitList, err := headCommit.CommitsBeforeUntil(ctx, gitRepo, baseCommit.ID.RefName()) if err != nil { return false, err } diff --git a/services/auth/auth_token.go b/services/auth/auth_token.go index 655c76fe31..f3bf723f22 100644 --- a/services/auth/auth_token.go +++ b/services/auth/auth_token.go @@ -57,6 +57,10 @@ func CheckAuthToken(ctx context.Context, value string) (*auth_model.AuthToken, e if subtle.ConstantTimeCompare([]byte(t.TokenHash), []byte(hex.EncodeToString(hashedToken[:]))) == 0 { // If an attacker steals a token and uses the token to create a new session the hash gets updated. // When the victim uses the old token the hashes don't match anymore and the victim should be notified about the compromised token. + // Revoke the token so the attacker's rotated token (which shares this ID) can no longer be used. + if err := auth_model.DeleteAuthTokenByID(ctx, t.ID); err != nil { + return nil, err + } return nil, ErrAuthTokenInvalidHash } diff --git a/services/auth/auth_token_test.go b/services/auth/auth_token_test.go index 01b562eb6a..ab24381d0a 100644 --- a/services/auth/auth_token_test.go +++ b/services/auth/auth_token_test.go @@ -10,6 +10,7 @@ import ( auth_model "gitea.dev/models/auth" "gitea.dev/models/unittest" "gitea.dev/modules/timeutil" + "gitea.dev/modules/util" "github.com/stretchr/testify/assert" ) @@ -62,7 +63,9 @@ func TestCheckAuthToken(t *testing.T) { assert.ErrorIs(t, err, ErrAuthTokenInvalidHash) assert.Nil(t, at2) - assert.NoError(t, auth_model.DeleteAuthTokenByID(t.Context(), at.ID)) + // a hash mismatch signals a compromised token, which must be revoked + _, err = auth_model.GetAuthTokenByID(t.Context(), at.ID) + assert.ErrorIs(t, err, util.ErrNotExist) }) t.Run("Valid", func(t *testing.T) { diff --git a/services/auth/signin.go b/services/auth/signin.go index b4ba50c250..516d5463bc 100644 --- a/services/auth/signin.go +++ b/services/auth/signin.go @@ -19,6 +19,8 @@ import ( _ "gitea.dev/services/auth/source/ldap" // register the ldap source _ "gitea.dev/services/auth/source/pam" // register the pam source _ "gitea.dev/services/auth/source/sspi" // register the sspi source + + "xorm.io/builder" ) // UserSignIn validates user name and password. @@ -27,9 +29,8 @@ func UserSignIn(ctx context.Context, username, password string) (*user_model.Use isEmail := false if strings.Contains(username, "@") { isEmail = true - emailAddress := user_model.EmailAddress{LowerEmail: strings.ToLower(strings.TrimSpace(username))} // check same email - has, err := db.GetEngine(ctx).Get(&emailAddress) + emailAddress, has, err := db.Get[user_model.EmailAddress](ctx, builder.Eq{"lower_email": strings.ToLower(strings.TrimSpace(username))}) if err != nil { return nil, nil, err } @@ -51,9 +52,23 @@ func UserSignIn(ctx context.Context, username, password string) (*user_model.Use } if user != nil { - hasUser, err := user_model.GetIndividualUser(ctx, user) - if err != nil { - return nil, nil, err + var hasUser bool + var err error + if user.ID > 0 { + user, err = user_model.GetUserByID(ctx, user.ID) + if err != nil && !user_model.IsErrUserNotExist(err) { + return nil, nil, err + } + if user != nil && user.Type != user_model.UserTypeIndividual { + return nil, nil, user_model.ErrUserNotExist{Name: username} + } + hasUser = user != nil + } else if user.LowerName != "" { + user, err = user_model.GetIndividualUserByName(ctx, user.LowerName) + if err != nil && !user_model.IsErrUserNotExist(err) { + return nil, nil, err + } + hasUser = user != nil } if hasUser { diff --git a/services/auth/source/oauth2/source_sync.go b/services/auth/source/oauth2/source_sync.go index 3123e54bbb..5b8511287e 100644 --- a/services/auth/source/oauth2/source_sync.go +++ b/services/auth/source/oauth2/source_sync.go @@ -61,13 +61,7 @@ func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *us } } - user := &user_model.User{ - LoginName: u.ExternalID, - LoginType: auth.OAuth2, - LoginSource: u.LoginSourceID, - } - - hasUser, err := user_model.GetIndividualUser(ctx, user) + user, hasUser, err := user_model.GetIndividualUserByLoginSource(ctx, auth.OAuth2, u.LoginSourceID, u.ExternalID) if err != nil { return err } @@ -77,13 +71,11 @@ func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *us // recognizes them as a valid user, they will be able to login // via their provider and reactivate their account. if shouldDisable { - log.Info("SyncExternalUsers[%s] disabling user %d", source.AuthSource.Name, user.ID) - return db.WithTx(ctx, func(ctx context.Context) error { if hasUser { + log.Info("SyncExternalUsers[%s] disabling user %d", source.AuthSource.Name, user.ID) user.IsActive = false - err := user_model.UpdateUserCols(ctx, user, "is_active") - if err != nil { + if err := user_model.UpdateUserCols(ctx, user, "is_active"); err != nil { return err } } diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go index bbbd9cf2d3..a29b9c028b 100644 --- a/services/automerge/automerge.go +++ b/services/automerge/automerge.go @@ -104,13 +104,13 @@ func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_m } func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) { - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + gitRepo, err := gitrepo.OpenRepository(repo) if err != nil { return nil, err } defer gitRepo.Close() - refs, err := gitRepo.GetRefsBySha(sha, "") + refs, err := gitRepo.GetRefsBySha(ctx, sha, "") if err != nil { return nil, err } @@ -181,14 +181,14 @@ func handlePullRequestAutoMerge(pullID int64, sha string) { } // check the sha is the same as pull request head commit id - baseGitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo) + baseGitRepo, err := gitrepo.OpenRepository(pr.BaseRepo) if err != nil { log.Error("OpenRepository: %v", err) return } defer baseGitRepo.Close() - headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + headCommitID, err := baseGitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) if err != nil { log.Error("GetRefCommitID: %v", err) return diff --git a/services/automergequeue/automergequeue.go b/services/automergequeue/automergequeue.go index 70556c2086..0f69059dd9 100644 --- a/services/automergequeue/automergequeue.go +++ b/services/automergequeue/automergequeue.go @@ -34,13 +34,13 @@ func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullReques return } - gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo) + gitRepo, err := gitrepo.OpenRepository(pull.BaseRepo) if err != nil { log.Error("OpenRepository: %v", err) return } defer gitRepo.Close() - commitID, err := gitRepo.GetRefCommitID(pull.GetGitHeadRefName()) + commitID, err := gitRepo.GetRefCommitID(ctx, pull.GetGitHeadRefName()) if err != nil { log.Error("GetRefCommitID: %v", err) return diff --git a/services/context/api.go b/services/context/api.go index 02ec2b7138..b99e722ed4 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -49,9 +49,10 @@ type APIContext struct { } // TokenCanAccessRepo reports whether the current API token is allowed to access the repository. -// A public-only token cannot reach a private repo; any other token is unrestricted by this check. +// A public-only token cannot reach a private repo or a repo owned by a non-public (limited or +// private) owner; any other token is unrestricted by this check. func (ctx *APIContext) TokenCanAccessRepo(repo *repo_model.Repository) bool { - return repo == nil || !ctx.PublicOnly || !repo.IsPrivate + return !ctx.PublicOnly || !publicOnlyTokenDeniedRepo(ctx, repo) } func init() { @@ -104,10 +105,6 @@ type APINotFound struct{} // swagger:response conflict type APIConflict struct{} -// APIRedirect is a redirect response -// swagger:response redirect -type APIRedirect struct{} - // APIString is a string response // swagger:response string type APIString string @@ -309,11 +306,11 @@ func RepoRefForAPI(next http.Handler) http.Handler { var err error switch refType { case git.RefTypeBranch: - ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName) + ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx, refName) case git.RefTypeTag: - ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(refName) + ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(ctx, refName) case git.RefTypeCommit: - ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName) + ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(ctx, refName) } if ctx.Repo.Commit == nil || errors.Is(err, util.ErrNotExist) { ctx.APIErrorNotFound("unable to find a git ref") diff --git a/services/context/context_template.go b/services/context/context_template.go index c458912e69..87877095ab 100644 --- a/services/context/context_template.go +++ b/services/context/context_template.go @@ -132,8 +132,9 @@ func (c TemplateContext) HeadMetaContentSecurityPolicy() template.HTML { // * Maybe this approach should be avoided, don't make the config system too complex, just let users use A return template.HTML(`= setting.UI.MaxDisplayFileSize { + if treeEntry.Blob(r.GitRepo).Size(ctx) >= setting.UI.MaxDisplayFileSize { return nil, nil, git.ErrNotExist{ID: "", RelPath: ".editorconfig"} } - reader, err := treeEntry.Blob().DataAsync() + reader, err := treeEntry.Blob(r.GitRepo).DataAsync(ctx) if err != nil { return nil, nil, err } @@ -589,6 +589,7 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare ctx.Repo.RepoLink = repo.Link() ctx.Data["RepoLink"] = ctx.Repo.RepoLink ctx.Data["FeedURL"] = ctx.Repo.RepoLink + ctx.Data["CloneButtonOriginLink"] = repo.CloneLink(ctx, ctx.Doer) // CloneButtonOriginLink may be rewritten to the WikiCloneLink by the router middleware unit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeExternalTracker) if err == nil { @@ -643,18 +644,6 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare // If multiple forks are available or if the user can fork to another account, but there is already a fork: open selection dialog ctx.Data["ShowForkModal"] = len(userAndOrgForks) > 1 || (canSignedUserFork && len(userAndOrgForks) > 0) - ctx.Data["RepoCloneLink"] = repo.CloneLink(ctx, ctx.Doer) - - cloneButtonShowHTTPS := !setting.Repository.DisableHTTPGit - cloneButtonShowSSH := !setting.SSH.Disabled && (ctx.IsSigned || setting.SSH.ExposeAnonymous) - if !cloneButtonShowHTTPS && !cloneButtonShowSSH { - // We have to show at least one link, so we just show the HTTPS - cloneButtonShowHTTPS = true - } - ctx.Data["CloneButtonShowHTTPS"] = cloneButtonShowHTTPS - ctx.Data["CloneButtonShowSSH"] = cloneButtonShowSSH - ctx.Data["CloneButtonOriginLink"] = ctx.Data["RepoCloneLink"] // it may be rewritten to the WikiCloneLink by the router middleware - ctx.Data["RepoSearchEnabled"] = setting.Indexer.RepoIndexerEnabled if setting.Indexer.RepoIndexerEnabled { ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) @@ -703,7 +692,7 @@ func repoAssignmentPrepareGitRepo(ctx *Context, data *repoAssignmentPrepareDataS ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo) if err != nil { if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "no such file or directory") { - log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RelativePath(), err) + log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.FullName(), err) ctx.Repo.Repository.MarkAsBrokenEmpty() // Only allow access to base of repo or settings if !repoAssignmentIsHomeOrSettings(ctx, data) { @@ -778,16 +767,6 @@ func repoAssignmentPrepareRepoTransfer(ctx *Context, data *repoAssignmentPrepare } } -func repoAssignmentHandleGoGet(ctx *Context, data *repoAssignmentPrepareDataStruct) { - repo := data.repo - if ctx.FormString("go-get") == "1" { - ctx.Data["GoGetImport"] = ComposeGoGetImport(ctx, repo.Owner.Name, repo.Name) - fullURLPrefix := repo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName) - ctx.Data["GoDocDirectory"] = fullURLPrefix + "{/dir}" - ctx.Data["GoDocFile"] = fullURLPrefix + "{/dir}/{file}#L{line}" - } -} - // RepoAssignment returns a middleware to handle repository assignment func RepoAssignment(ctx *Context) { repoAssignmentPreCheck(ctx) @@ -804,7 +783,6 @@ func RepoAssignment(ctx *Context) { repoAssignmentPrepareRepoTransfer, repoAssignmentPrepareBranches, repoAssignmentPreparePullRequests, - repoAssignmentHandleGoGet, } for _, f := range funcs { f(ctx, prepareData) @@ -852,7 +830,9 @@ func getRefNameLegacy(ctx *Base, repo *Repository, reqPath, extraRef string) (re func getRefName(ctx *Base, repo *Repository, path string, refType git.RefType) string { switch refType { case git.RefTypeBranch: - ref := getRefNameFromPath(repo, path, repo.GitRepo.IsBranchExist) + ref := getRefNameFromPath(repo, path, func(s string) bool { + return repo.GitRepo.IsBranchExist(ctx, s) + }) if len(ref) == 0 { // check if ref is HEAD parts := strings.Split(path, "/") @@ -882,7 +862,9 @@ func getRefName(ctx *Base, repo *Repository, path string, refType git.RefType) s return ref case git.RefTypeTag: - return getRefNameFromPath(repo, path, repo.GitRepo.IsTagExist) + return getRefNameFromPath(repo, path, func(s string) bool { + return repo.GitRepo.IsTagExist(ctx, s) + }) case git.RefTypeCommit: parts := strings.Split(path, "/") if git.IsStringLikelyCommitID(repo.GetObjectFormat(), parts[0], 7) { @@ -893,7 +875,7 @@ func getRefName(ctx *Base, repo *Repository, path string, refType git.RefType) s if parts[0] == headRefName { // HEAD ref points to last default branch commit - commit, err := repo.GitRepo.GetBranchCommit(repo.Repository.DefaultBranch) + commit, err := repo.GitRepo.GetBranchCommit(ctx, repo.Repository.DefaultBranch) if err != nil { return "" } @@ -924,7 +906,7 @@ func RepoRefByDefaultBranch() func(*Context) { return func(ctx *Context) { ctx.Repo.RefFullName = git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch) ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch - ctx.Repo.Commit, _ = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName) + ctx.Repo.Commit, _ = ctx.Repo.GitRepo.GetBranchCommit(ctx, ctx.Repo.BranchName) ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount(ctx) ctx.Data["RefFullName"] = ctx.Repo.RefFullName ctx.Data["BranchName"] = ctx.Repo.BranchName @@ -958,18 +940,18 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { if reqPath == "" { refShortName = ctx.Repo.Repository.DefaultBranch if !gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, refShortName) { - brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 1) + brs, _, err := ctx.Repo.GitRepo.GetBranchNames(ctx, 0, 1) if err == nil && len(brs) != 0 { refShortName = brs[0] } else if len(brs) == 0 { - log.Error("No branches in non-empty repository %s", ctx.Repo.Repository.RelativePath()) + log.Error("No branches in non-empty repository %s", ctx.Repo.Repository.FullName()) } else { log.Error("GetBranches error: %v", err) } } ctx.Repo.RefFullName = git.RefNameFromBranch(refShortName) ctx.Repo.BranchName = refShortName - ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refShortName) + ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx, refShortName) if err == nil { ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() } else { @@ -998,7 +980,7 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { ctx.Repo.BranchName = refShortName ctx.Repo.RefFullName = git.RefNameFromBranch(refShortName) - ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refShortName) + ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx, refShortName) if err != nil { ctx.ServerError("GetBranchCommit", err) return @@ -1007,7 +989,7 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { } else if refType == git.RefTypeTag && gitrepo.IsTagExist(ctx, ctx.Repo.Repository, refShortName) { ctx.Repo.RefFullName = git.RefNameFromTag(refShortName) - ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(refShortName) + ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(ctx, refShortName) if err != nil { if git.IsErrNotExist(err) { ctx.NotFound(err) @@ -1021,7 +1003,7 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { ctx.Repo.RefFullName = git.RefNameFromCommit(refShortName) ctx.Repo.CommitID = refShortName - ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refShortName) + ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(ctx, refShortName) if err != nil { ctx.NotFound(err) return diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go index 1a6f4b0104..135cddedb8 100644 --- a/services/contexttest/context_tests.go +++ b/services/contexttest/context_tests.go @@ -141,7 +141,7 @@ func LoadRepoCommit(t *testing.T, ctx gocontext.Context) { assert.FailNow(t, "context is not *context.Context or *context.APIContext") } - gitRepo, err := gitrepo.OpenRepository(ctx, repo.Repository) + gitRepo, err := gitrepo.OpenRepository(repo.Repository) require.NoError(t, err) t.Cleanup(func() { gitRepo.Close() @@ -152,7 +152,7 @@ func LoadRepoCommit(t *testing.T, ctx gocontext.Context) { if repo.RefFullName.IsPull() { repo.BranchName = repo.RefFullName.ShortName() } - repo.Commit, err = gitRepo.GetCommit(repo.RefFullName.String()) + repo.Commit, err = gitRepo.GetCommit(ctx, repo.RefFullName.String()) require.NoError(t, err) } @@ -185,7 +185,7 @@ func LoadGitRepo(t *testing.T, ctx gocontext.Context) { } assert.NoError(t, repo.Repository.LoadOwner(ctx)) var err error - repo.GitRepo, err = gitrepo.OpenRepository(ctx, repo.Repository) + repo.GitRepo, err = gitrepo.OpenRepository(repo.Repository) assert.NoError(t, err) } diff --git a/services/convert/action_test.go b/services/convert/action_test.go index 0cf72d28a4..2616e3aff5 100644 --- a/services/convert/action_test.go +++ b/services/convert/action_test.go @@ -62,7 +62,7 @@ func TestGetActionWorkflow_FallbackRef(t *testing.T) { repoDir := buildWorkflowTestRepo(t) - gitRepo, err := git.OpenRepository(ctx, repoDir) + gitRepo, err := git.OpenRepositoryLocal(repoDir) require.NoError(t, err) defer gitRepo.Close() @@ -177,3 +177,13 @@ func TestToActionWorkflowJob_StepStatusIsIndependentOfJobStatus(t *testing.T) { assert.Equal(t, "completed", apiJob.Steps[1].Status, "step 1 status") assert.Equal(t, "failure", apiJob.Steps[1].Conclusion, "step 1 conclusion") } + +func TestToActionsStatus_Cancelling(t *testing.T) { + action, conclusion := ToActionsStatus(actions_model.StatusCancelling) + assert.Equal(t, "in_progress", action) + assert.Empty(t, conclusion) +} + +func TestToWorkflowRunAction_Cancelling(t *testing.T) { + assert.Equal(t, "in_progress", ToWorkflowRunAction(actions_model.StatusCancelling)) +} diff --git a/services/convert/convert.go b/services/convert/convert.go index d1437de286..dd1510031d 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -7,6 +7,7 @@ package convert import ( "bytes" "context" + "errors" "fmt" "net/url" "path" @@ -29,6 +30,7 @@ import ( "gitea.dev/modules/actions" "gitea.dev/modules/container" "gitea.dev/modules/git" + "gitea.dev/modules/gitrepo" "gitea.dev/modules/httplib" "gitea.dev/modules/log" "gitea.dev/modules/setting" @@ -412,7 +414,7 @@ func ToWorkflowRunAction(status actions_model.Status) (action string) { switch status { case actions_model.StatusWaiting, actions_model.StatusBlocked: action = "requested" - case actions_model.StatusRunning: + case actions_model.StatusRunning, actions_model.StatusCancelling: action = "in_progress" default: if status.IsDone() { @@ -430,7 +432,7 @@ func ToActionsStatus(status actions_model.Status) (action, conclusion string) { action = "queued" // "waiting" is a naming conflict of the webhook between Gitea and GitHub Actions case actions_model.StatusBlocked: action = "waiting" // naming conflict (as above) - case actions_model.StatusRunning: + case actions_model.StatusRunning, actions_model.StatusCancelling: action = "in_progress" default: action = "completed" @@ -521,7 +523,7 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task }, nil } -func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, refName git.RefName, folder string, entry *git.TreeEntry) *api.ActionWorkflow { +func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit, refName git.RefName, folder string, entry *git.TreeEntry) *api.ActionWorkflow { cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions) cfg := cfgUnit.ActionsConfig() @@ -552,7 +554,7 @@ func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, co createdAt := commit.Author.When updatedAt := commit.Author.When - content, err := actions.GetContentFromEntry(entry) + content, err := actions.GetContentFromEntry(ctx, gitRepo, entry) name := entry.Name() if err == nil { workflow, err := model.ReadWorkflow(bytes.NewReader(content)) @@ -582,31 +584,31 @@ func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, co } func ListActionWorkflows(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository) ([]*api.ActionWorkflow, error) { - defaultBranchCommit, err := gitrepo.GetBranchCommit(repo.DefaultBranch) + defaultBranchCommit, err := gitrepo.GetBranchCommit(ctx, repo.DefaultBranch) if err != nil { return nil, err } - folder, entries, err := actions.ListWorkflows(defaultBranchCommit) + folder, entries, err := actions.ListWorkflows(ctx, gitrepo, defaultBranchCommit) if err != nil { return nil, err } workflows := make([]*api.ActionWorkflow, len(entries)) for i, entry := range entries { - workflows[i] = getActionWorkflowEntry(ctx, repo, defaultBranchCommit, git.RefNameFromBranch(repo.DefaultBranch), folder, entry) + workflows[i] = getActionWorkflowEntry(ctx, repo, gitrepo, defaultBranchCommit, git.RefNameFromBranch(repo.DefaultBranch), folder, entry) } return workflows, nil } -func GetActionWorkflow(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository, workflowID string) (*api.ActionWorkflow, error) { - defaultBranchCommit, err := gitrepo.GetBranchCommit(repo.DefaultBranch) +func GetActionWorkflow(ctx context.Context, gitRepo *git.Repository, repo *repo_model.Repository, workflowID string) (*api.ActionWorkflow, error) { + defaultBranchCommit, err := gitRepo.GetBranchCommit(ctx, repo.DefaultBranch) if err != nil { return nil, err } - return getActionWorkflowFromCommit(ctx, repo, defaultBranchCommit, git.RefNameFromBranch(repo.DefaultBranch), workflowID) + return getActionWorkflowFromCommit(ctx, repo, gitRepo, defaultBranchCommit, git.RefNameFromBranch(repo.DefaultBranch), workflowID) } func GetActionWorkflowByRef(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository, workflowID string, ref git.RefName) (*api.ActionWorkflow, error) { @@ -614,33 +616,91 @@ func GetActionWorkflowByRef(ctx context.Context, gitrepo *git.Repository, repo * return nil, util.NewNotExistErrorf("workflow %q not found", workflowID) } - refCommitID, err := gitrepo.GetRefCommitID(ref.String()) + refCommitID, err := gitrepo.GetRefCommitID(ctx, ref.String()) if err != nil { return nil, err } - refCommit, err := gitrepo.GetCommit(refCommitID) + refCommit, err := gitrepo.GetCommit(ctx, refCommitID) if err != nil { return nil, err } - return getActionWorkflowFromCommit(ctx, repo, refCommit, ref, workflowID) + return getActionWorkflowFromCommit(ctx, repo, gitrepo, refCommit, ref, workflowID) } -func getActionWorkflowFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, refName git.RefName, workflowID string) (*api.ActionWorkflow, error) { - folder, entries, err := actions.ListWorkflows(commit) +func getActionWorkflowFromCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit, refName git.RefName, workflowID string) (*api.ActionWorkflow, error) { + folder, entries, err := actions.ListWorkflows(ctx, gitRepo, commit) if err != nil { return nil, err } for _, entry := range entries { if entry.Name() == workflowID { - return getActionWorkflowEntry(ctx, repo, commit, refName, folder, entry), nil + return getActionWorkflowEntry(ctx, repo, gitRepo, commit, refName, folder, entry), nil } } return nil, util.NewNotExistErrorf("workflow %q not found", workflowID) } +// GetScopedActionWorkflow resolves a scoped workflow definition (under SCOPED_WORKFLOW_DIRS) from the source repo at commitSHA. +func GetScopedActionWorkflow(ctx context.Context, sourceGitRepo *git.Repository, sourceRepo *repo_model.Repository, workflowID, commitSHA string) (*api.ActionWorkflow, error) { + commit, err := sourceGitRepo.GetCommit(ctx, commitSHA) + if err != nil { + return nil, err + } + + folder, entries, err := actions.ListScopedWorkflows(ctx, sourceGitRepo, commit) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if entry.Name() == workflowID { + // An empty ref pins HTMLURL to commit (the run's WorkflowCommitSHA) rather than the moving default branch. + wf := getActionWorkflowEntry(ctx, sourceRepo, sourceGitRepo, commit, "", folder, entry) + // TODO: a scoped workflow has no repo-level representation on the source: the workflow API scans WORKFLOW_DIRS (not SCOPED_WORKFLOW_DIRS), + // and the badge only reflects the source's repo-level runs, so neither link resolves a scoped workflow. + // Blank them for now and populate once a scoped-aware workflow/badge endpoint exists. + wf.URL = "" + wf.BadgeURL = "" + return wf, nil + } + } + + return nil, util.NewNotExistErrorf("scoped workflow %q not found", workflowID) +} + +// ResolveActionWorkflowForRun returns the api.ActionWorkflow describing a run's workflow definition. +// For a scoped run the definition lives in the source repo (run.WorkflowRepoID @ run.WorkflowCommitSHA) under SCOPED_WORKFLOW_DIRS, +// not in the consuming repo, so it is resolved against the source repo. +func ResolveActionWorkflowForRun(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun) (*api.ActionWorkflow, error) { + if run.IsScopedRun { + sourceRepo, err := repo_model.GetRepositoryByID(ctx, run.WorkflowRepoID) + if err != nil { + return nil, err + } + sourceGitRepo, err := gitrepo.OpenRepository(sourceRepo) + if err != nil { + return nil, err + } + defer sourceGitRepo.Close() + return GetScopedActionWorkflow(ctx, sourceGitRepo, sourceRepo, run.WorkflowID, run.WorkflowCommitSHA) + } + + gitRepo, err := gitrepo.OpenRepository(repo) + if err != nil { + return nil, err + } + defer gitRepo.Close() + + convertedWorkflow, err := GetActionWorkflowByRef(ctx, gitRepo, repo, run.WorkflowID, git.RefName(run.Ref)) + if err != nil && errors.Is(err, util.ErrNotExist) { + convertedWorkflow, err = GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) + } + return convertedWorkflow, err +} + // ToActionArtifact convert a actions_model.ActionArtifact to an api.ActionArtifact func ToActionArtifact(repo *repo_model.Repository, art *actions_model.ActionArtifact) (*api.ActionArtifact, error) { url := fmt.Sprintf("%s/actions/artifacts/%d", repo.APIURL(), art.ID) @@ -804,7 +864,7 @@ func ToOrganization(ctx context.Context, org *organization.Organization) *api.Or Description: org.Description, Website: org.Website, Location: org.Location, - Visibility: api.UserVisibility(org.Visibility.String()), + Visibility: api.VisibilityString(org.Visibility.String()), RepoAdminChangeTeamAccess: org.RepoAdminChangeTeamAccess, } } diff --git a/services/convert/pull.go b/services/convert/pull.go index cdc79eb88f..74560837e1 100644 --- a/services/convert/pull.go +++ b/services/convert/pull.go @@ -145,9 +145,9 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u apiPullRequest.Closed = pr.Issue.ClosedUnix.AsTimePtr() } - gitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo) + gitRepo, err := gitrepo.OpenRepository(pr.BaseRepo) if err != nil { - log.Error("OpenRepository[%s]: %v", pr.BaseRepo.RelativePath(), err) + log.Error("OpenRepository[%s]: %v", pr.BaseRepo.FullName(), err) return nil } defer gitRepo.Close() @@ -159,7 +159,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u } if exist { - baseCommit, err = gitRepo.GetBranchCommit(pr.BaseBranch) + baseCommit, err = gitRepo.GetBranchCommit(ctx, pr.BaseBranch) if err != nil && !git.IsErrNotExist(err) { log.Error("GetCommit[%s]: %v", baseBranch, err) return nil @@ -171,7 +171,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u } if pr.Flow == issues_model.PullRequestFlowAGit { - apiPullRequest.Head.Sha, err = gitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + apiPullRequest.Head.Sha, err = gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) if err != nil { log.Error("GetRefCommitID[%s]: %v", pr.GetGitHeadRefName(), err) return nil @@ -191,9 +191,9 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u apiPullRequest.Head.RepoID = pr.HeadRepo.ID apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p) - headGitRepo, err := gitrepo.OpenRepository(ctx, pr.HeadRepo) + headGitRepo, err := gitrepo.OpenRepository(pr.HeadRepo) if err != nil { - log.Error("OpenRepository[%s]: %v", pr.HeadRepo.RelativePath(), err) + log.Error("OpenRepository[%s]: %v", pr.HeadRepo.FullName(), err) return nil } defer headGitRepo.Close() @@ -211,7 +211,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u ) if !exist { - headCommitID, err := headGitRepo.GetRefCommitID(apiPullRequest.Head.Ref) + headCommitID, err := headGitRepo.GetRefCommitID(ctx, apiPullRequest.Head.Ref) if err != nil && !git.IsErrNotExist(err) { log.Error("GetCommit[%s]: %v", pr.HeadBranch, err) return nil @@ -221,7 +221,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u endCommitID = headCommitID } } else { - commit, err := headGitRepo.GetBranchCommit(pr.HeadBranch) + commit, err := headGitRepo.GetBranchCommit(ctx, pr.HeadBranch) if err != nil && !git.IsErrNotExist(err) { log.Error("GetCommit[%s]: %v", headBranch, err) return nil @@ -247,13 +247,13 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u } if len(apiPullRequest.Head.Sha) == 0 && len(apiPullRequest.Head.Ref) != 0 { - baseGitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo) + baseGitRepo, err := gitrepo.OpenRepository(pr.BaseRepo) if err != nil { - log.Error("OpenRepository[%s]: %v", pr.BaseRepo.RelativePath(), err) + log.Error("OpenRepository[%s]: %v", pr.BaseRepo.FullName(), err) return nil } defer baseGitRepo.Close() - refs, err := baseGitRepo.GetRefsFiltered(apiPullRequest.Head.Ref) + refs, err := baseGitRepo.GetRefsFiltered(ctx, apiPullRequest.Head.Ref) if err != nil { log.Error("GetRefsFiltered[%s]: %v", apiPullRequest.Head.Ref, err) return nil @@ -329,7 +329,7 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs return nil, err } - gitRepo, err := gitrepo.OpenRepository(ctx, baseRepo) + gitRepo, err := gitrepo.OpenRepository(baseRepo) if err != nil { return nil, err } @@ -452,13 +452,13 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs if pr.Flow == issues_model.PullRequestFlowAGit { apiPullRequest.Head.Name = "" } - apiPullRequest.Head.Sha, err = gitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + apiPullRequest.Head.Sha, err = gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) if err != nil { log.Error("GetRefCommitID[%s]: %v", pr.GetGitHeadRefName(), err) } if len(apiPullRequest.Head.Sha) == 0 && len(apiPullRequest.Head.Ref) != 0 { - refs, err := gitRepo.GetRefsFiltered(apiPullRequest.Head.Ref) + refs, err := gitRepo.GetRefsFiltered(ctx, apiPullRequest.Head.Ref) if err != nil { log.Error("GetRefsFiltered[%s]: %v", apiPullRequest.Head.Ref, err) return nil, err diff --git a/services/convert/user.go b/services/convert/user.go index 525eb46818..d9ed124f04 100644 --- a/services/convert/user.go +++ b/services/convert/user.go @@ -65,7 +65,7 @@ func toUser(ctx context.Context, user *user_model.User, signed, authed bool) *ap StarredRepos: user.NumStars, } - result.Visibility = api.UserVisibility(user.Visibility.String()) + result.Visibility = api.VisibilityString(user.Visibility.String()) // hide primary email if API caller is anonymous or user keep email private if signed && (!user.KeepEmailPrivate || authed) { diff --git a/services/convert/user_test.go b/services/convert/user_test.go index 3ef7609f04..c42fc1455f 100644 --- a/services/convert/user_test.go +++ b/services/convert/user_test.go @@ -29,11 +29,11 @@ func TestUser_ToUser(t *testing.T) { apiUser = toUser(t.Context(), user1, false, false) assert.False(t, apiUser.IsAdmin) - assert.Equal(t, api.UserVisibilityPublic, apiUser.Visibility) + assert.Equal(t, api.VisibilityStringPublic, apiUser.Visibility) user31 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31, IsAdmin: false, Visibility: api.VisibleTypePrivate}) apiUser = toUser(t.Context(), user31, true, true) assert.False(t, apiUser.IsAdmin) - assert.Equal(t, api.UserVisibilityPrivate, apiUser.Visibility) + assert.Equal(t, api.VisibilityStringPrivate, apiUser.Visibility) } diff --git a/services/doctor/misc.go b/services/doctor/misc.go index b097fa6ab4..2f3d0a157a 100644 --- a/services/doctor/misc.go +++ b/services/doctor/misc.go @@ -11,6 +11,7 @@ import ( "gitea.dev/models/db" repo_model "gitea.dev/models/repo" user_model "gitea.dev/models/user" + "gitea.dev/modules/git" "gitea.dev/modules/gitrepo" "gitea.dev/modules/log" "gitea.dev/modules/structs" @@ -162,7 +163,7 @@ func checkCommitGraph(ctx context.Context, logger log.Logger, autofix bool) erro if !isExist { numNeedUpdate++ if autofix { - if err := gitrepo.WriteCommitGraph(ctx, repo); err != nil { + if err := git.WriteCommitGraph(ctx, repo); err != nil { logger.Error("Unable to write commit-graph in %s. Error: %v", repo.FullName(), err) return err } diff --git a/services/git/commit.go b/services/git/commit.go index bc0516b5d6..662e050713 100644 --- a/services/git/commit.go +++ b/services/git/commit.go @@ -7,7 +7,6 @@ import ( "context" asymkey_model "gitea.dev/models/asymkey" - "gitea.dev/models/db" git_model "gitea.dev/models/git" "gitea.dev/models/gituser" repo_model "gitea.dev/models/repo" @@ -72,20 +71,27 @@ func ConvertFromGitCommit(ctx context.Context, commits []*git.Commit, repo *repo // ParseCommitsWithStatus checks commits latest statuses and calculates its worst status state func ParseCommitsWithStatus(ctx context.Context, oldCommits []*asymkey_model.SignCommit, repo *repo_model.Repository) ([]*git_model.SignCommitWithStatuses, error) { - newCommits := make([]*git_model.SignCommitWithStatuses, 0, len(oldCommits)) + if len(oldCommits) == 0 { + return nil, nil + } + commitIDs := make([]string, 0, len(oldCommits)) for _, c := range oldCommits { - commit := &git_model.SignCommitWithStatuses{ - SignCommit: c, - } - statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commit.GitCommit.ID.String(), db.ListOptionsAll) - if err != nil { - return nil, err - } + commitIDs = append(commitIDs, c.GitCommit.ID.String()) + } + statusMap, err := git_model.GetLatestCommitStatusForRepoCommitIDs(ctx, repo.ID, commitIDs) + if err != nil { + return nil, err + } - commit.Statuses = statuses - commit.Status = git_model.CalcCommitStatus(statuses) - newCommits = append(newCommits, commit) + newCommits := make([]*git_model.SignCommitWithStatuses, 0, len(oldCommits)) + for _, c := range oldCommits { + statuses := statusMap[c.GitCommit.ID.String()] + newCommits = append(newCommits, &git_model.SignCommitWithStatuses{ + SignCommit: c, + Statuses: statuses, + Status: git_model.CalcCommitStatus(statuses), + }) } return newCommits, nil } diff --git a/services/git/compare.go b/services/git/compare.go index 23f6ffe75f..a19e6fd7bf 100644 --- a/services/git/compare.go +++ b/services/git/compare.go @@ -72,7 +72,7 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito // if they are not the same repository, then we need to fetch the base commit into the head repository // because we will use headGitRepo in the following code if baseRepo.ID != headRepo.ID { - exist := headGitRepo.IsReferenceExist(compareInfo.BaseCommitID) + exist := headGitRepo.IsReferenceExist(ctx, compareInfo.BaseCommitID) if !exist { if err := gitrepo.FetchRemoteCommit(ctx, headRepo, baseRepo, compareInfo.BaseCommitID); err != nil { return compareInfo, fmt.Errorf("FetchRemoteCommit: %w", err) @@ -108,6 +108,6 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito // Count number of changed files. // TODO: This probably should be removed as we need to use shortstat elsewhere // Now there is git diff --shortstat but this appears to be slower than simply iterating with --nameonly - compareInfo.NumFiles, err = headGitRepo.GetDiffNumChangedFiles(compareInfo.BaseCommitID, compareInfo.HeadCommitID, directComparison) + compareInfo.NumFiles, err = headGitRepo.GetDiffNumChangedFiles(ctx, compareInfo.BaseCommitID, compareInfo.HeadCommitID, directComparison) return compareInfo, err } diff --git a/services/gitdiff/git_diff_tree.go b/services/gitdiff/git_diff_tree.go index 7ef9bc6165..9b50fb8e5f 100644 --- a/services/gitdiff/git_diff_tree.go +++ b/services/gitdiff/git_diff_tree.go @@ -52,7 +52,7 @@ func GetDiffTree(ctx context.Context, gitRepo *git.Repository, useMergeBase bool } func runGitDiffTree(ctx context.Context, gitRepo *git.Repository, useMergeBase bool, baseSha, headSha string) ([]*DiffTreeRecord, error) { - useMergeBase, baseCommitID, headCommitID, err := validateGitDiffTreeArguments(gitRepo, useMergeBase, baseSha, headSha) + useMergeBase, baseCommitID, headCommitID, err := validateGitDiffTreeArguments(ctx, gitRepo, useMergeBase, baseSha, headSha) if err != nil { return nil, err } @@ -73,14 +73,14 @@ func runGitDiffTree(ctx context.Context, gitRepo *git.Repository, useMergeBase b return parseGitDiffTree(strings.NewReader(stdout)) } -func validateGitDiffTreeArguments(gitRepo *git.Repository, useMergeBase bool, baseSha, headSha string) (shouldUseMergeBase bool, resolvedBaseSha, resolvedHeadSha string, err error) { +func validateGitDiffTreeArguments(ctx context.Context, gitRepo *git.Repository, useMergeBase bool, baseSha, headSha string) (shouldUseMergeBase bool, resolvedBaseSha, resolvedHeadSha string, err error) { // if the head is empty its an error if headSha == "" { return false, "", "", errors.New("headSha is empty") } // if the head commit doesn't exist its and error - headCommit, err := gitRepo.GetCommit(headSha) + headCommit, err := gitRepo.GetCommit(ctx, headSha) if err != nil { return false, "", "", fmt.Errorf("failed to get commit headSha: %v", err) } @@ -91,7 +91,7 @@ func validateGitDiffTreeArguments(gitRepo *git.Repository, useMergeBase bool, ba // if the headCommit has no parent we should use an empty commit // this can happen when we are generating a diff against an orphaned commit if headCommit.ParentCount() == 0 { - objectFormat, err := gitRepo.GetObjectFormat() + objectFormat, err := gitRepo.GetObjectFormat(ctx) if err != nil { return false, "", "", err } @@ -100,7 +100,7 @@ func validateGitDiffTreeArguments(gitRepo *git.Repository, useMergeBase bool, ba return false, objectFormat.EmptyTree().String(), headCommitID, nil } - baseCommit, err := headCommit.Parent(0) + baseCommit, err := headCommit.Parent(ctx, gitRepo, 0) if err != nil { return false, "", "", fmt.Errorf("baseSha is '', attempted to use parent of commit %s, got error: %v", headCommit.ID.String(), err) } @@ -108,7 +108,7 @@ func validateGitDiffTreeArguments(gitRepo *git.Repository, useMergeBase bool, ba } // try and get the base commit - baseCommit, err := gitRepo.GetCommit(baseSha) + baseCommit, err := gitRepo.GetCommit(ctx, baseSha) // propagate the error if we couldn't get the base commit if err != nil { return useMergeBase, "", "", fmt.Errorf("failed to get base commit %s: %v", baseSha, err) diff --git a/services/gitdiff/git_diff_tree_test.go b/services/gitdiff/git_diff_tree_test.go index 7b20316085..da55224dfc 100644 --- a/services/gitdiff/git_diff_tree_test.go +++ b/services/gitdiff/git_diff_tree_test.go @@ -205,7 +205,7 @@ func TestGitDiffTree(t *testing.T) { for _, tt := range test { t.Run(tt.Name, func(t *testing.T) { - gitRepo, err := git.OpenRepository(t.Context(), tt.RepoPath) + gitRepo, err := git.OpenRepositoryLocal(tt.RepoPath) assert.NoError(t, err) defer gitRepo.Close() @@ -414,7 +414,7 @@ func TestGitDiffTreeErrors(t *testing.T) { for _, tt := range test { t.Run(tt.Name, func(t *testing.T) { - gitRepo, err := git.OpenRepository(t.Context(), tt.RepoPath) + gitRepo, err := git.OpenRepositoryLocal(tt.RepoPath) assert.NoError(t, err) defer gitRepo.Close() diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 686acb2629..1effa75403 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -38,6 +38,7 @@ import ( "gitea.dev/modules/setting" "gitea.dev/modules/svg" "gitea.dev/modules/translation" + "gitea.dev/modules/typesniffer" "gitea.dev/modules/util" "github.com/alecthomas/chroma/v2" @@ -465,6 +466,14 @@ type DiffFile struct { highlightRender diffVarMutable[chroma.Lexer] // cache render (atm: lexer) for current file, only detect once for line-by-line mode highlightedLeftLines diffVarMutable[map[int]template.HTML] highlightedRightLines diffVarMutable[map[int]template.HTML] + + // image diff and csv diff need some of the following fields + LeftBlob, RightBlob *git.Blob + LeftBlobSize, RightBlobSize int64 + LeftBlobMimeType, RightBlobMimeType string + + IsBlobTypeImage bool + IsBlobTypeCsv bool } // GetType returns type of diff file. @@ -472,31 +481,64 @@ func (diffFile *DiffFile) GetType() int { return int(diffFile.Type) } -type DiffLimitedContent struct { - LeftContent, RightContent *limitByteWriter +type DiffRenderDetail struct { + needTailSection bool + leftLineCount, rightLineCount int + leftContent, rightContent *limitByteWriter } -// GetTailSectionAndLimitedContent creates a fake DiffLineSection if the last section is not the end of the file -func (diffFile *DiffFile) GetTailSectionAndLimitedContent(leftCommit, rightCommit *git.Commit) (_ *DiffSection, diffLimitedContent DiffLimitedContent) { - var leftLineCount, rightLineCount int - diffLimitedContent = DiffLimitedContent{} - if diffFile.IsBin || diffFile.IsLFSFile { - return nil, diffLimitedContent +func (diffFile *DiffFile) prepareDiffRenderDetail(ctx context.Context, gitRepo *git.Repository, leftCommit, rightCommit *git.Commit) (ret DiffRenderDetail) { + if diffFile.IsLFSFile { + return ret } + + // pre-fetch the blob info & content + // * for "bin" type: need the pre-fetched buffer to detect content type (e.g.: help to render image diff) + // * for "text" type: need to read up to "highlight limit size" to do full-file-highlighting + contentLimit := util.Iif(diffFile.IsBin, typesniffer.SniffContentSize, MaxFullFileHighlightSizeLimit) + var leftLineCount, rightLineCount int + var leftBlobType, rightBlobType typesniffer.SniffedType if (diffFile.Type == DiffFileDel || diffFile.Type == DiffFileChange) && leftCommit != nil { - leftLineCount, diffLimitedContent.LeftContent = getCommitFileLineCountAndLimitedContent(leftCommit, diffFile.OldName) + c := getCommitFileBlobAndLimitedContent(ctx, gitRepo, leftCommit, diffFile.OldName, contentLimit) + diffFile.LeftBlob, diffFile.LeftBlobSize, leftLineCount, ret.leftContent = c.gitBlob, c.blobSize, c.lineCount, c.limitedContent + leftBlobType = typesniffer.DetectContentType(ret.leftContent.buf.Bytes()) } if (diffFile.Type == DiffFileAdd || diffFile.Type == DiffFileChange) && rightCommit != nil { - rightLineCount, diffLimitedContent.RightContent = getCommitFileLineCountAndLimitedContent(rightCommit, diffFile.OldName) + c := getCommitFileBlobAndLimitedContent(ctx, gitRepo, rightCommit, diffFile.OldName, contentLimit) + diffFile.RightBlob, diffFile.RightBlobSize, rightLineCount, ret.rightContent = c.gitBlob, c.blobSize, c.lineCount, c.limitedContent + rightBlobType = typesniffer.DetectContentType(ret.rightContent.buf.Bytes()) } - if len(diffFile.Sections) == 0 || diffFile.Type != DiffFileChange { - return nil, diffLimitedContent + + isFileTypeImage := func(st typesniffer.SniffedType) bool { + return st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()) } + isFileTypeCsv := func(name string) bool { + extension := strings.ToLower(path.Ext(name)) + return extension == ".csv" || extension == ".tsv" + } + diffFile.LeftBlobMimeType, diffFile.RightBlobMimeType = leftBlobType.GetMimeType(), rightBlobType.GetMimeType() + diffFile.IsBlobTypeImage = isFileTypeImage(leftBlobType) || isFileTypeImage(rightBlobType) + diffFile.IsBlobTypeCsv = isFileTypeCsv(diffFile.Name) + + if diffFile.IsBin || len(diffFile.Sections) == 0 || diffFile.Type != DiffFileChange { + return ret + } + + // check whether the text file diff needs a tail section lastSection := diffFile.Sections[len(diffFile.Sections)-1] lastLine := lastSection.Lines[len(lastSection.Lines)-1] if leftLineCount <= lastLine.LeftIdx || rightLineCount <= lastLine.RightIdx { - return nil, diffLimitedContent + return ret } + ret.needTailSection = true + return ret +} + +func (diffFile *DiffFile) addTailSection(detail DiffRenderDetail) { + // if the last diff section still doesn't reach to the end of file, we need to add a "tail section" to + // make users can expand the remaining unchanged lines to see the full file content. + lastSection := diffFile.Sections[len(diffFile.Sections)-1] + lastLine := lastSection.Lines[len(lastSection.Lines)-1] tailDiffLine := &DiffLine{ Type: DiffLineSection, Content: " ", @@ -505,12 +547,12 @@ func (diffFile *DiffFile) GetTailSectionAndLimitedContent(leftCommit, rightCommi Path: diffFile.Name, LastLeftIdx: lastLine.LeftIdx, LastRightIdx: lastLine.RightIdx, - LeftIdx: leftLineCount, - RightIdx: rightLineCount, + LeftIdx: detail.leftLineCount, + RightIdx: detail.rightLineCount, }, } tailSection := &DiffSection{FileName: diffFile.Name, Lines: []*DiffLine{tailDiffLine}} - return tailSection, diffLimitedContent + diffFile.Sections = append(diffFile.Sections, tailSection) } // GetDiffFileName returns the name of the diff file, or its old name in case it was deleted @@ -577,17 +619,24 @@ func (l *limitByteWriter) Write(p []byte) (n int, err error) { return l.buf.Write(p) } -func getCommitFileLineCountAndLimitedContent(commit *git.Commit, filePath string) (lineCount int, limitWriter *limitByteWriter) { - blob, err := commit.GetBlobByPath(filePath) +func getCommitFileBlobAndLimitedContent(ctx context.Context, gitRepo *git.Repository, commit *git.Commit, filePath string, limit int) (ret struct { + gitBlob *git.Blob + blobSize int64 + lineCount int + limitedContent *limitByteWriter +}, +) { + var err error + ret.limitedContent = &limitByteWriter{limit: limit} + ret.gitBlob, err = commit.GetBlobByPath(ctx, gitRepo, filePath) if err != nil { - return 0, nil + return ret } - w := &limitByteWriter{limit: MaxFullFileHighlightSizeLimit + 1} - lineCount, err = blob.GetBlobLineCount(w) + ret.blobSize, ret.lineCount, err = ret.gitBlob.GetBlobLineCount(ctx, ret.limitedContent) if err != nil { - return 0, nil + return ret } - return lineCount, w + return ret } // Diff represents a difference between two git trees. @@ -1248,7 +1297,7 @@ type DiffOptions struct { DirectComparison bool } -func guessBeforeCommitForDiff(gitRepo *git.Repository, beforeCommitID string, afterCommit *git.Commit) (actualBeforeCommit *git.Commit, actualBeforeCommitID git.ObjectID, err error) { +func guessBeforeCommitForDiff(ctx context.Context, gitRepo *git.Repository, beforeCommitID string, afterCommit *git.Commit) (actualBeforeCommit *git.Commit, actualBeforeCommitID git.ObjectID, err error) { commitObjectFormat := afterCommit.ID.Type() isBeforeCommitIDEmpty := beforeCommitID == "" || beforeCommitID == commitObjectFormat.EmptyObjectID().String() @@ -1256,9 +1305,9 @@ func guessBeforeCommitForDiff(gitRepo *git.Repository, beforeCommitID string, af actualBeforeCommitID = commitObjectFormat.EmptyTree() } else { if isBeforeCommitIDEmpty { - actualBeforeCommit, err = afterCommit.Parent(0) + actualBeforeCommit, err = afterCommit.Parent(ctx, gitRepo, 0) } else { - actualBeforeCommit, err = gitRepo.GetCommit(beforeCommitID) + actualBeforeCommit, err = gitRepo.GetCommit(ctx, beforeCommitID) } if err != nil { return nil, nil, err @@ -1275,16 +1324,18 @@ func guessBeforeCommitForDiff(gitRepo *git.Repository, beforeCommitID string, af func getDiffBasic(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, files ...string) (_ *Diff, beforeCommit, afterCommit *git.Commit, err error) { repoPath := gitRepo.Path - afterCommit, err = gitRepo.GetCommit(opts.AfterCommitID) + afterCommit, err = gitRepo.GetCommit(ctx, opts.AfterCommitID) if err != nil { return nil, nil, nil, err } - beforeCommit, beforeCommitID, err := guessBeforeCommitForDiff(gitRepo, opts.BeforeCommitID, afterCommit) + beforeCommit, beforeCommitID, err := guessBeforeCommitForDiff(ctx, gitRepo, opts.BeforeCommitID, afterCommit) if err != nil { return nil, nil, nil, err } + // HINT: GIT-DIFF-HIGHLIGHT-LINE-NUMBER: git doesn't treat CR(\r) as EOL, CR is just a plain char which can appear anywhere in the diff output + // Since we have to do full-file-highlighting for the diff result, we need to make sure the highlighted lines exactly match the git's diff output. cmdDiff := gitcmd.NewCommand(). AddArguments("diff", "--src-prefix=\\a/", "--dst-prefix=\\b/"). AddArguments(opts.WhitespaceBehavior...). @@ -1338,7 +1389,7 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit startTime := time.Now() - checker, err := attribute.NewBatchChecker(gitRepo, opts.AfterCommitID, []string{attribute.LinguistVendored, attribute.LinguistGenerated, attribute.LinguistLanguage, attribute.GitlabLanguage, attribute.Diff}) + checker, err := attribute.NewBatchChecker(ctx, gitRepo, opts.AfterCommitID, []string{attribute.LinguistVendored, attribute.LinguistGenerated, attribute.LinguistLanguage, attribute.GitlabLanguage, attribute.Diff}) if err != nil { return nil, err } @@ -1360,7 +1411,7 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit // Populate Submodule URLs if diffFile.SubmoduleDiffInfo != nil { - diffFile.SubmoduleDiffInfo.PopulateURL(repoLink, diffFile, beforeCommit, afterCommit) + diffFile.SubmoduleDiffInfo.PopulateURL(ctx, repoLink, gitRepo, diffFile, beforeCommit, afterCommit) } if !isVendored.Has() { @@ -1372,19 +1423,22 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit isGenerated = optional.Some(analyze.IsGenerated(diffFile.Name)) } diffFile.IsGenerated = isGenerated.Value() - tailSection, limitedContent := diffFile.GetTailSectionAndLimitedContent(beforeCommit, afterCommit) - if tailSection != nil { - diffFile.Sections = append(diffFile.Sections, tailSection) + + // prepare more details for the rendering, e.g.: blob (file) size, type, limited content to highlight, etc + renderDetail := diffFile.prepareDiffRenderDetail(ctx, gitRepo, beforeCommit, afterCommit) + if renderDetail.needTailSection { + diffFile.addTailSection(renderDetail) } - shouldFullFileHighlight := attrDiff.Value() == "" // only do highlight if no custom diff command + // only do highlight for text files which have no custom diff command + shouldFullFileHighlight := !diffFile.IsBin && !diffFile.IsLFSFile && attrDiff.Value() == "" shouldFullFileHighlight = shouldFullFileHighlight && time.Since(startTime) < MaxFullFileHighlightTimeLimit if shouldFullFileHighlight { - if limitedContent.LeftContent != nil { - diffFile.highlightedLeftLines.value = highlightCodeLinesForDiffFile(diffFile, true /* left */, limitedContent.LeftContent.buf.Bytes()) + if renderDetail.leftContent != nil { + diffFile.highlightedLeftLines.value = highlightCodeLinesForDiffFile(diffFile, true /* left */, renderDetail.leftContent.buf.Bytes()) } - if limitedContent.RightContent != nil { - diffFile.highlightedRightLines.value = highlightCodeLinesForDiffFile(diffFile, false /* right */, limitedContent.RightContent.buf.Bytes()) + if renderDetail.rightContent != nil { + diffFile.highlightedRightLines.value = highlightCodeLinesForDiffFile(diffFile, false /* right */, renderDetail.rightContent.buf.Bytes()) } } } @@ -1402,11 +1456,15 @@ func highlightCodeLinesForDiffFile(diffFile *DiffFile, isLeft bool, rawContent [ } func highlightCodeLines(name, lang string, sections []*DiffSection, isLeft bool, rawContent []byte) map[int]template.HTML { - if setting.Git.DisableDiffHighlight || len(rawContent) > MaxFullFileHighlightSizeLimit { + if setting.Git.DisableDiffHighlight || len(rawContent) >= MaxFullFileHighlightSizeLimit { return nil } - content := util.UnsafeBytesToString(charset.ToUTF8(rawContent, charset.ConvertOpts{})) + // HINT: GIT-DIFF-HIGHLIGHT-LINE-NUMBER: it should handle all CR(\r) before highlight to make line numbers match + if strings.Contains(content, "\r") { + content = strings.ReplaceAll(content, "\r\n", "\n") + content = strings.ReplaceAll(content, "\r", "␍") + } lexer := highlight.DetectChromaLexerByFileName(name, lang) highlightedNewContent := highlight.RenderCodeByLexer(lexer, content) unsafeLines := highlight.UnsafeSplitHighlightedLines(highlightedNewContent) @@ -1434,12 +1492,12 @@ type DiffShortStat struct { } func GetDiffShortStat(ctx context.Context, repoStorage gitrepo.Repository, gitRepo *git.Repository, beforeCommitID, afterCommitID string) (*DiffShortStat, error) { - afterCommit, err := gitRepo.GetCommit(afterCommitID) + afterCommit, err := gitRepo.GetCommit(ctx, afterCommitID) if err != nil { return nil, err } - _, actualBeforeCommitID, err := guessBeforeCommitForDiff(gitRepo, beforeCommitID, afterCommit) + _, actualBeforeCommitID, err := guessBeforeCommitForDiff(ctx, gitRepo, beforeCommitID, afterCommit) if err != nil { return nil, err } @@ -1468,7 +1526,7 @@ func SyncUserSpecificDiff(ctx context.Context, userID int64, pull *issues_model. latestCommit = pull.HeadBranch // opts.AfterCommitID is preferred because it handles PRs from forks correctly and the branch name doesn't } - changedFiles, errIgnored := gitRepo.GetFilesChangedBetween(review.CommitSHA, latestCommit) + changedFiles, errIgnored := gitRepo.GetFilesChangedBetween(ctx, review.CommitSHA, latestCommit) // There are way too many possible errors. // Examples are various git errors such as the commit the review was based on was gc'ed and hence doesn't exist anymore as well as unrecoverable errors where we should serve a 500 response // Due to the current architecture and physical limitation of needing to compare explicit error messages, we can only choose one approach without the code getting ugly @@ -1542,19 +1600,19 @@ func CommentAsDiff(ctx context.Context, c *issues_model.Comment) (*Diff, error) } // GeneratePatchForUnchangedLine creates a patch showing code context for an unchanged line -func GeneratePatchForUnchangedLine(gitRepo *git.Repository, commitID, treePath string, line int64, contextLines int) (string, error) { - commit, err := gitRepo.GetCommit(commitID) +func GeneratePatchForUnchangedLine(ctx context.Context, gitRepo *git.Repository, commitID, treePath string, line int64, contextLines int) (string, error) { + commit, err := gitRepo.GetCommit(ctx, commitID) if err != nil { return "", fmt.Errorf("GetCommit: %w", err) } - entry, err := commit.GetTreeEntryByPath(treePath) + entry, err := commit.GetTreeEntryByPath(ctx, gitRepo, treePath) if err != nil { return "", fmt.Errorf("GetTreeEntryByPath: %w", err) } - blob := entry.Blob() - dataRc, err := blob.DataAsync() + blob := entry.Blob(gitRepo) + dataRc, err := blob.DataAsync(ctx) if err != nil { return "", fmt.Errorf("DataAsync: %w", err) } diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index fd61a09972..1b5ea391e1 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -601,7 +601,7 @@ func TestDiffLine_GetCommentSide(t *testing.T) { } func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) { - gitRepo, err := git.OpenRepository(t.Context(), "../../modules/git/tests/repos/repo5_pulls") + gitRepo, err := git.OpenRepositoryLocal("../../modules/git/tests/repos/repo5_pulls") require.NoError(t, err) defer gitRepo.Close() @@ -1143,6 +1143,19 @@ func TestHighlightCodeLines(t *testing.T) { 1: `b` + nl, }, ret) }) + t.Run("CharCR", func(t *testing.T) { + diffFile := &DiffFile{ + Name: "a.txt", + Sections: []*DiffSection{ + { + Lines: []*DiffLine{{LeftIdx: 1}, {LeftIdx: 2}}, + }, + }, + } + ret := highlightCodeLinesForDiffFile(diffFile, true, []byte("a\rb\r\nc")) + assert.Equal(t, "a␍b\n", string(ret[0])) + assert.Equal(t, `c`, string(ret[1])) + }) } func TestSyncUserSpecificDiff_UpdatedFiles(t *testing.T) { @@ -1175,7 +1188,7 @@ D test2.txt D test10.txt` require.NoError(t, gitcmd.NewCommand("fast-import").WithDir(pull.BaseRepo.RepoPath()).WithStdinBytes([]byte(stdin)).Run(t.Context())) - gitRepo, err := git.OpenRepository(t.Context(), pull.BaseRepo.RepoPath()) + gitRepo, err := git.OpenRepositoryLocal(pull.BaseRepo.RepoPath()) assert.NoError(t, err) defer gitRepo.Close() diff --git a/services/gitdiff/submodule.go b/services/gitdiff/submodule.go index 9fffd13060..592eb2fe48 100644 --- a/services/gitdiff/submodule.go +++ b/services/gitdiff/submodule.go @@ -20,7 +20,7 @@ type SubmoduleDiffInfo struct { PreviousRefID string } -func (si *SubmoduleDiffInfo) PopulateURL(repoLink string, diffFile *DiffFile, leftCommit, rightCommit *git.Commit) { +func (si *SubmoduleDiffInfo) PopulateURL(ctx context.Context, repoLink string, gitRepo *git.Repository, diffFile *DiffFile, leftCommit, rightCommit *git.Commit) { si.SubmoduleName = diffFile.Name submoduleCommit := rightCommit // If the submodule is added or updated, check at the right commit if diffFile.IsDeleted { @@ -31,7 +31,7 @@ func (si *SubmoduleDiffInfo) PopulateURL(repoLink string, diffFile *DiffFile, le } submoduleFullPath := diffFile.GetDiffFileName() - submodule, err := submoduleCommit.GetSubModule(submoduleFullPath) + submodule, err := submoduleCommit.GetSubModule(ctx, gitRepo, submoduleFullPath) if err != nil { log.Error("Unable to PopulateURL for submodule %q: GetSubModule: %v", submoduleFullPath, err) return // ignore the error, do not cause 500 errors for end users diff --git a/services/issue/comments.go b/services/issue/comments.go index ff6c9fccf5..89943c83cd 100644 --- a/services/issue/comments.go +++ b/services/issue/comments.go @@ -19,6 +19,8 @@ import ( "gitea.dev/modules/timeutil" git_service "gitea.dev/services/git" notify_service "gitea.dev/services/notify" + + "xorm.io/builder" ) // CreateRefComment creates a commit reference comment to issue. @@ -34,10 +36,10 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod } // Check if same reference from same commit has already existed. - has, err := db.GetEngine(ctx).Get(&issues_model.Comment{ - Type: issues_model.CommentTypeCommitRef, - IssueID: issue.ID, - CommitSHA: commitSHA, + has, err := db.Exist[issues_model.Comment](ctx, builder.Eq{ + "`type`": issues_model.CommentTypeCommitRef, + "issue_id": issue.ID, + "commit_sha": commitSHA, }) if err != nil { return fmt.Errorf("check reference comment: %w", err) @@ -184,7 +186,7 @@ func LoadCommentPushCommits(ctx context.Context, c *issues_model.Comment) error } defer closer.Close() - c.Commits, err = git_service.ConvertFromGitCommit(ctx, gitRepo.GetCommitsFromIDs(data.CommitIDs), c.Issue.Repo, "") // no current ref sub path for PR commit list + c.Commits, err = git_service.ConvertFromGitCommit(ctx, gitRepo.GetCommitsFromIDs(ctx, data.CommitIDs), c.Issue.Repo, "") // no current ref sub path for PR commit list if err != nil { log.Debug("ConvertFromGitCommit: %v", err) // no need to show 500 error to end user when the commit does not exist } else { diff --git a/services/issue/pull.go b/services/issue/pull.go index 260ac5ceae..39a56ba233 100644 --- a/services/issue/pull.go +++ b/services/issue/pull.go @@ -55,21 +55,21 @@ func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullReque return nil, nil } - repo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo) + repo, err := gitrepo.OpenRepository(pr.BaseRepo) if err != nil { return nil, err } defer repo.Close() - commit, err := repo.GetBranchCommit(pr.BaseRepo.DefaultBranch) + commit, err := repo.GetBranchCommit(ctx, pr.BaseRepo.DefaultBranch) if err != nil { return nil, err } var data string for _, file := range codeOwnerFiles { - if blob, err := commit.GetBlobByPath(file); err == nil { - data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize) + if blob, err := commit.GetBlobByPath(ctx, repo, file); err == nil { + data, err = blob.GetBlobContent(ctx, setting.UI.MaxDisplayFileSize) if err == nil { break } @@ -91,7 +91,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullReque } // https://github.com/go-gitea/gitea/issues/29763, we need to get the files changed // between the merge base and the head commit but not the base branch and the head commit - changedFiles, err := repo.GetFilesChangedBetween(mergeBase, pr.GetGitHeadRefName()) + changedFiles, err := repo.GetFilesChangedBetween(ctx, mergeBase, pr.GetGitHeadRefName()) if err != nil { return nil, err } diff --git a/services/issue/template.go b/services/issue/template.go index c1a0ff2fdd..c53df65efc 100644 --- a/services/issue/template.go +++ b/services/issue/template.go @@ -4,6 +4,7 @@ package issue import ( + "context" "fmt" "net/url" "path" @@ -47,17 +48,17 @@ func GetDefaultTemplateConfig() api.IssueConfig { // GetTemplateConfig loads the given issue config file. // It never returns a nil config. -func GetTemplateConfig(gitRepo *git.Repository, path string, commit *git.Commit) (api.IssueConfig, error) { +func GetTemplateConfig(ctx context.Context, gitRepo *git.Repository, path string, commit *git.Commit) (api.IssueConfig, error) { if gitRepo == nil { return GetDefaultTemplateConfig(), nil } - treeEntry, err := commit.GetTreeEntryByPath(path) + treeEntry, err := commit.GetTreeEntryByPath(ctx, gitRepo, path) if err != nil { return GetDefaultTemplateConfig(), err } - reader, err := treeEntry.Blob().DataAsync() + reader, err := treeEntry.Blob(gitRepo).DataAsync(ctx) if err != nil { log.Debug("DataAsync: %v", err) return GetDefaultTemplateConfig(), nil @@ -109,7 +110,7 @@ func IsTemplateConfig(path string) bool { // ParseTemplatesFromDefaultBranch parses the issue templates in the repo's default branch, // returns valid templates and the errors of invalid template files (the errors map is guaranteed to be non-nil). -func ParseTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repository) (ret struct { +func ParseTemplatesFromDefaultBranch(ctx context.Context, repo *repo.Repository, gitRepo *git.Repository) (ret struct { IssueTemplates []*api.IssueTemplate TemplateErrors map[string]error }, @@ -119,18 +120,18 @@ func ParseTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Reposit return ret } - commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch) + commit, err := gitRepo.GetBranchCommit(ctx, repo.DefaultBranch) if err != nil { return ret } for _, dirName := range templateDirCandidates { - tree, err := commit.SubTree(dirName) + tree, err := commit.SubTree(ctx, gitRepo, dirName) if err != nil { log.Debug("get sub tree of %s: %v", dirName, err) continue } - entries, err := tree.ListEntries() + entries, err := tree.ListEntries(ctx, gitRepo) if err != nil { log.Debug("list entries in %s: %v", dirName, err) return ret @@ -140,7 +141,7 @@ func ParseTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Reposit continue } fullName := path.Join(dirName, entry.Name()) - if it, err := template.UnmarshalFromEntry(entry, dirName); err != nil { + if it, err := template.UnmarshalFromEntry(ctx, gitRepo, entry, dirName); err != nil { ret.TemplateErrors[fullName] = err } else { if !strings.HasPrefix(it.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/ @@ -155,35 +156,35 @@ func ParseTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Reposit // GetTemplateConfigFromDefaultBranch returns the issue config for this repo. // It never returns a nil config. -func GetTemplateConfigFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repository) (api.IssueConfig, error) { +func GetTemplateConfigFromDefaultBranch(ctx context.Context, repo *repo.Repository, gitRepo *git.Repository) (api.IssueConfig, error) { if repo.IsEmpty { return GetDefaultTemplateConfig(), nil } - commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch) + commit, err := gitRepo.GetBranchCommit(ctx, repo.DefaultBranch) if err != nil { return GetDefaultTemplateConfig(), err } for _, configName := range templateConfigCandidates { - if _, err := commit.GetTreeEntryByPath(configName + ".yaml"); err == nil { - return GetTemplateConfig(gitRepo, configName+".yaml", commit) + if _, err := commit.GetTreeEntryByPath(ctx, gitRepo, configName+".yaml"); err == nil { + return GetTemplateConfig(ctx, gitRepo, configName+".yaml", commit) } - if _, err := commit.GetTreeEntryByPath(configName + ".yml"); err == nil { - return GetTemplateConfig(gitRepo, configName+".yml", commit) + if _, err := commit.GetTreeEntryByPath(ctx, gitRepo, configName+".yml"); err == nil { + return GetTemplateConfig(ctx, gitRepo, configName+".yml", commit) } } return GetDefaultTemplateConfig(), nil } -func HasTemplatesOrContactLinks(repo *repo.Repository, gitRepo *git.Repository) bool { - ret := ParseTemplatesFromDefaultBranch(repo, gitRepo) +func HasTemplatesOrContactLinks(ctx context.Context, repo *repo.Repository, gitRepo *git.Repository) bool { + ret := ParseTemplatesFromDefaultBranch(ctx, repo, gitRepo) if len(ret.IssueTemplates) > 0 { return true } - issueConfig, _ := GetTemplateConfigFromDefaultBranch(repo, gitRepo) + issueConfig, _ := GetTemplateConfigFromDefaultBranch(ctx, repo, gitRepo) return len(issueConfig.ContactLinks) > 0 } diff --git a/services/lfs/server.go b/services/lfs/server.go index 6340b06252..8c7de2ccb6 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -254,6 +254,17 @@ func BatchHandler(ctx *context.Context) { var responseObject *lfs_module.ObjectResponse if isUpload { + if exists && meta == nil { + // The object exists in the content store but is not linked to this + // repo. Do not auto-link it based on cross-repo access: the token + // only authorizes this repo, and for deploy keys ctx.Doer is the + // repo owner, so trusting it here would let a single-repo key pull + // objects from any repo the owner can see. Require proof of + // possession by making the client upload the (hash-verified) bytes, + // and treat it as a new upload for size-limit enforcement below. + exists = false + } + var err *lfs_module.ObjectError if !exists && setting.LFS.MaxFileSize > 0 && p.Size > setting.LFS.MaxFileSize { err = &lfs_module.ObjectError{ @@ -262,25 +273,6 @@ func BatchHandler(ctx *context.Context) { } } - if exists && meta == nil { - accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid) - if err != nil { - log.Error("Unable to check if LFS MetaObject [%s] is accessible. Error: %v", p.Oid, err) - writeStatus(ctx, http.StatusInternalServerError) - return - } - if accessible { - _, err := git_model.NewLFSMetaObject(ctx, repository.ID, p) - if err != nil { - log.Error("Unable to create LFS MetaObject [%s] for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err) - writeStatus(ctx, http.StatusInternalServerError) - return - } - } else { - exists = false - } - } - responseObject = buildObjectResponse(rc, p, false, !exists, err) } else { var err *lfs_module.ObjectError @@ -337,13 +329,17 @@ func UploadHandler(ctx *context.Context) { uploadOrVerify := func() error { if exists { - accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid) - if err != nil { - log.Error("Unable to check if LFS MetaObject [%s] is accessible. Error: %v", p.Oid, err) + // The bytes already exist in the content store. Only skip proof of + // possession when the object is already linked to *this* repo; never + // trust cross-repo access (ctx.Doer is the repo owner for deploy keys), + // which would let a caller link an object it cannot produce. + meta, err := git_model.GetLFSMetaObjectByOid(ctx, repository.ID, p.Oid) + if err != nil && err != git_model.ErrLFSObjectNotExist { + log.Error("Unable to get LFS MetaObject [%s]. Error: %v", p.Oid, err) return err } - if !accessible { - // The file exists but the user has no access to it. + if meta == nil { + // The file exists but is not linked to this repo. // The upload gets verified by hashing and size comparison to prove access to it. hash := sha256.New() written, err := io.Copy(hash, ctx.Req.Body) diff --git a/services/mailer/mail_issue_common.go b/services/mailer/mail_issue_common.go index 816fb864d6..1197969176 100644 --- a/services/mailer/mail_issue_common.go +++ b/services/mailer/mail_issue_common.go @@ -99,7 +99,9 @@ func composeIssueCommentMessages(ctx context.Context, comment *mailComment, lang } } locale := translation.NewLocale(lang) - + if lang == "mock" { + locale = &translation.MockLocale{} + } mailMeta := map[string]any{ "locale": locale, "FallbackSubject": fallback, diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go index cbe3bb08bb..2925328307 100644 --- a/services/mailer/mail_test.go +++ b/services/mailer/mail_test.go @@ -18,10 +18,14 @@ import ( actions_model "gitea.dev/models/actions" activities_model "gitea.dev/models/activities" + "gitea.dev/models/asymkey" + git_model "gitea.dev/models/git" + "gitea.dev/models/gituser" issues_model "gitea.dev/models/issues" repo_model "gitea.dev/models/repo" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" + "gitea.dev/modules/git" "gitea.dev/modules/markup" "gitea.dev/modules/setting" "gitea.dev/modules/storage" @@ -556,3 +560,32 @@ func TestEmbedBase64Images(t *testing.T) { assert.Equal(t, expected, string(resultMailBody)) }) } + +func TestMailPullRequestPush(t *testing.T) { + doer, _, issue, comment := prepareMailerTest(t) + mc := &mailComment{ + Issue: issue, + Comment: comment, + Doer: doer, + } + issue.IsPull = true + issue.PullRequest = &issues_model.PullRequest{BaseRepo: mc.Issue.Repo} + mc.Comment.Type = issues_model.CommentTypePullRequestPush + mc.Comment.Commits = []*git_model.SignCommitWithStatuses{ + { + SignCommit: &asymkey.SignCommit{ + UserCommit: &gituser.UserCommit{ + GitCommit: &git.Commit{ + CommitMessage: git.CommitMessage{MessageRaw: "test commit msg"}, + ID: git.Sha1ObjectFormat.EmptyObjectID(), + }, + }, + }, + }, + } + + msgs, err := composeIssueCommentMessages(t.Context(), mc, "mock", []*user_model.User{{Name: "Test", Email: "test@gitea.com"}}, false, "pull request push") + require.NoError(t, err) + assert.Contains(t, msgs[0].Body, `0000000000 - test commit msg`) + assert.Contains(t, msgs[0].Body, ``) +} diff --git a/services/markup/renderhelper_codepreview.go b/services/markup/renderhelper_codepreview.go index 962cced1a2..8a34b28714 100644 --- a/services/markup/renderhelper_codepreview.go +++ b/services/markup/renderhelper_codepreview.go @@ -50,28 +50,28 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie return "", util.ErrPermissionDenied } - gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo) + gitRepo, err := gitrepo.OpenRepository(dbRepo) if err != nil { return "", err } defer gitRepo.Close() - commit, err := gitRepo.GetCommit(opts.CommitID) + commit, err := gitRepo.GetCommit(ctx, opts.CommitID) if err != nil { return "", err } language, _ := languagestats.GetFileLanguage(ctx, gitRepo, opts.CommitID, opts.FilePath) - blob, err := commit.GetBlobByPath(opts.FilePath) + blob, err := commit.GetBlobByPath(ctx, gitRepo, opts.FilePath) if err != nil { return "", err } - if blob.Size() > setting.UI.MaxDisplayFileSize { + if blob.Size(ctx) > setting.UI.MaxDisplayFileSize { return "", errors.New("file is too large") } - dataRc, err := blob.DataAsync() + dataRc, err := blob.DataAsync(ctx) if err != nil { return "", err } diff --git a/services/migrations/common.go b/services/migrations/common.go index e9895d00c2..ba809e4045 100644 --- a/services/migrations/common.go +++ b/services/migrations/common.go @@ -22,6 +22,9 @@ func WarnAndNotice(fmtStr string, args ...any) { } func hasBaseURL(toCheck, baseURL string) bool { + if baseURL == "" { + return false + } if len(baseURL) > 0 && baseURL[len(baseURL)-1] != '/' { baseURL += "/" } diff --git a/services/migrations/dump.go b/services/migrations/dump.go index 949f2e79b8..3e3b537ebd 100644 --- a/services/migrations/dump.go +++ b/services/migrations/dump.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "io" - "net/http" "net/url" "os" "path/filepath" @@ -49,7 +48,7 @@ type RepositoryDumper struct { prHeadCache map[string]string } -// NewRepositoryDumper creates an gitea Uploader +// NewRepositoryDumper creates a gitea Uploader func NewRepositoryDumper(ctx context.Context, baseDir, repoOwner, repoName string, opts base.MigrateOptions) (*RepositoryDumper, error) { baseDir = filepath.Join(baseDir, repoOwner, repoName) if err := os.MkdirAll(baseDir, os.ModePerm); err != nil { @@ -158,7 +157,9 @@ func (g *RepositoryDumper) CreateRepo(ctx context.Context, repo *base.Repository if err != nil { return fmt.Errorf("Clone: %w", err) } - if err := git.WriteCommitGraph(ctx, repoPath); err != nil { + + repoLocal := gitcmd.RepositoryUnmanaged(repoPath) + if err := git.WriteCommitGraph(ctx, repoLocal); err != nil { return err } @@ -169,7 +170,7 @@ func (g *RepositoryDumper) CreateRepo(ctx context.Context, repo *base.Repository if err := os.MkdirAll(wikiPath, os.ModePerm); err != nil { return fmt.Errorf("Failed to remove %s: %w", wikiPath, err) } - + wikiLocal := gitcmd.RepositoryUnmanaged(wikiPath) if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{ Mirror: true, Quiet: true, @@ -181,13 +182,13 @@ func (g *RepositoryDumper) CreateRepo(ctx context.Context, repo *base.Repository if err := os.RemoveAll(wikiPath); err != nil { return fmt.Errorf("Failed to remove %s: %w", wikiPath, err) } - } else if err := git.WriteCommitGraph(ctx, wikiPath); err != nil { + } else if err := git.WriteCommitGraph(ctx, wikiLocal); err != nil { return err } } } - g.gitRepo, err = git.OpenRepository(ctx, g.gitPath()) + g.gitRepo, err = git.OpenRepositoryLocal(g.gitPath()) return err } @@ -310,7 +311,9 @@ func (g *RepositoryDumper) CreateReleases(_ context.Context, releases ...*base.R } defer rc.Close() } else { - resp, err := http.Get(*asset.DownloadURL) + // use the migration client so the fetch (including any redirect) is + // validated against the migration host allow/block list + resp, err := getMigrationHTTPClient().Get(*asset.DownloadURL) if err != nil { return err } @@ -450,8 +453,10 @@ func (g *RepositoryDumper) handlePullRequest(ctx context.Context, pr *base.PullR } // SECURITY: We will assume that the pr.PatchURL has been checked - // pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe - resp, err := http.Get(u) // TODO: This probably needs to use the downloader as there may be rate limiting issues here + // pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe. + // Use the migration client so an http(s) PatchURL (and any redirect it follows) is + // validated against the migration host allow/block list at dial time. + resp, err := getMigrationHTTPClient().Get(u) if err != nil { return err } @@ -516,7 +521,7 @@ func (g *RepositoryDumper) handlePullRequest(ctx context.Context, pr *base.PullR remote = "head-pr-" + strconv.FormatInt(pr.Number, 10) } // ... now add the remote - err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true) + err := g.gitRepo.AddRemote(ctx, remote, pr.Head.CloneURL, true) if err != nil { log.Error("PR #%d in %s/%s AddRemote[%s] failed: %v", pr.Number, g.repoOwner, g.repoName, remote, err) } else { @@ -543,10 +548,10 @@ func (g *RepositoryDumper) handlePullRequest(ctx context.Context, pr *base.PullR localRef = git.SanitizeRefPattern(oldHeadOwnerName + "/" + pr.Head.Ref) // ... Now we must assert that this does not exist - if g.gitRepo.IsBranchExist(localRef) { + if g.gitRepo.IsBranchExist(ctx, localRef) { localRef = "head-pr-" + strconv.FormatInt(pr.Number, 10) + "/" + localRef i := 0 - for g.gitRepo.IsBranchExist(localRef) { + for g.gitRepo.IsBranchExist(ctx, localRef) { if i > 5 { // ... We tried, we really tried but this is just a seriously unfriendly repo return fmt.Errorf("unable to create unique local reference from %s", pr.Head.Ref) @@ -578,7 +583,7 @@ func (g *RepositoryDumper) handlePullRequest(ctx context.Context, pr *base.PullR // 5. Now if pr.Head.SHA == "" we should recover this to the head of this branch if pr.Head.SHA == "" { - headSha, err := g.gitRepo.GetBranchCommitID(localRef) + headSha, err := g.gitRepo.GetBranchCommitID(ctx, localRef) if err != nil { log.Error("unable to get head SHA of local head for PR #%d from %s in %s/%s. Error: %v", pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err) return nil diff --git a/services/migrations/error.go b/services/migrations/error.go index 2dd5540f0e..455e9e16c7 100644 --- a/services/migrations/error.go +++ b/services/migrations/error.go @@ -7,7 +7,7 @@ package migrations import ( "errors" - "github.com/google/go-github/v88/github" + "github.com/google/go-github/v89/github" ) // ErrRepoNotCreated returns the error that repository not created diff --git a/services/migrations/gitea_downloader.go b/services/migrations/gitea_downloader.go index c754d70090..6ec70e2bbb 100644 --- a/services/migrations/gitea_downloader.go +++ b/services/migrations/gitea_downloader.go @@ -84,7 +84,7 @@ func NewGiteaDownloader(ctx context.Context, baseURL, repoPath, username, passwo baseURL, gitea_sdk.SetToken(token), gitea_sdk.SetBasicAuth(username, password), - gitea_sdk.SetHTTPClient(NewMigrationHTTPClient()), + gitea_sdk.SetHTTPClient(newMigrationHTTPClient()), ) if err != nil { log.Error(fmt.Sprintf("Failed to create NewGiteaDownloader for: %s. Error: %v", baseURL, err)) @@ -273,7 +273,7 @@ func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Rele Created: rel.CreatedAt, } - httpClient := NewMigrationHTTPClient() + httpClient := newMigrationHTTPClient() for _, asset := range rel.Attachments { assetID := asset.ID // Don't optimize this, for closure we need a local variable diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index d195d69e1e..8379e448f5 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -56,7 +56,7 @@ type GiteaLocalUploader struct { gitServiceType structs.GitServiceType } -// NewGiteaLocalUploader creates an gitea Uploader via gitea API v1 +// NewGiteaLocalUploader creates a gitea Uploader via gitea API v1 func NewGiteaLocalUploader(_ context.Context, doer *user_model.User, repoOwner, repoName string) *GiteaLocalUploader { return &GiteaLocalUploader{ doer: doer, @@ -139,13 +139,13 @@ func (g *GiteaLocalUploader) CreateRepo(ctx context.Context, repo *base.Reposito if err != nil { return err } - g.gitRepo, err = gitrepo.OpenRepository(ctx, g.repo) + g.gitRepo, err = gitrepo.OpenRepository(g.repo) if err != nil { return err } // detect object format from git repository and update to database - objectFormat, err := g.gitRepo.GetObjectFormat() + objectFormat, err := g.gitRepo.GetObjectFormat(ctx) if err != nil { return err } @@ -298,7 +298,7 @@ func (g *GiteaLocalUploader) CreateReleases(ctx context.Context, releases ...*ba // calc NumCommits if possible if rel.TagName != "" { - commit, err := g.gitRepo.GetTagCommit(rel.TagName) + commit, err := g.gitRepo.GetTagCommit(ctx, rel.TagName) if !git.IsErrNotExist(err) { if err != nil { return fmt.Errorf("GetTagCommit[%v]: %w", rel.TagName, err) @@ -340,7 +340,9 @@ func (g *GiteaLocalUploader) CreateReleases(ctx context.Context, releases ...*ba return err } } else if asset.DownloadURL != nil { - rc, err = uri.Open(*asset.DownloadURL) + // use the migration client so the fetch (including any redirect) is + // validated against the migration host allow/block list + rc, err = uri.OpenWithClient(*asset.DownloadURL, getMigrationHTTPClient()) if err != nil { return err } @@ -585,8 +587,10 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(ctx context.Context, pr *ba } // SECURITY: We will assume that the pr.PatchURL has been checked - // pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe - ret, err := uri.Open(pr.PatchURL) // TODO: This probably needs to use the downloader as there may be rate limiting issues here + // pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe. + // Use the migration client so an http(s) PatchURL (and any redirect it follows) is + // validated against the migration host allow/block list at dial time. + ret, err := uri.OpenWithClient(pr.PatchURL, getMigrationHTTPClient()) if err != nil { return err } @@ -628,7 +632,7 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(ctx context.Context, pr *ba remote = "head-pr-" + strconv.FormatInt(pr.Number, 10) } // ... now add the remote - err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true) + err := g.gitRepo.AddRemote(ctx, remote, pr.Head.CloneURL, true) if err != nil { log.Error("PR #%d in %s/%s AddRemote[%s] failed: %v", pr.Number, g.repoOwner, g.repoName, remote, err) } else { @@ -647,10 +651,10 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(ctx context.Context, pr *ba localRef = git.SanitizeRefPattern(pr.Head.OwnerName + "/" + pr.Head.Ref) // ... Now we must assert that this does not exist - if g.gitRepo.IsBranchExist(localRef) { + if g.gitRepo.IsBranchExist(ctx, localRef) { localRef = "head-pr-" + strconv.FormatInt(pr.Number, 10) + "/" + localRef i := 0 - for g.gitRepo.IsBranchExist(localRef) { + for g.gitRepo.IsBranchExist(ctx, localRef) { if i > 5 { // ... We tried, we really tried but this is just a seriously unfriendly repo return head, nil @@ -677,7 +681,7 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(ctx context.Context, pr *ba // 5. Now if pr.Head.SHA == "" we should recover this to the head of this branch if pr.Head.SHA == "" { - headSha, err := g.gitRepo.GetBranchCommitID(localRef) + headSha, err := g.gitRepo.GetBranchCommitID(ctx, localRef) if err != nil { log.Error("unable to get head SHA of local head for PR #%d from %s in %s/%s. Error: %v", pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err) return head, nil @@ -882,7 +886,7 @@ func (g *GiteaLocalUploader) CreateReviews(ctx context.Context, reviews ...*base continue } - headCommitID, err := g.gitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + headCommitID, err := g.gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) if err != nil { log.Warn("PR #%d GetRefCommitID[%s] in %s/%s: %v, all review comments will be ignored", pr.Index, pr.GetGitHeadRefName(), g.repoOwner, g.repoName, err) continue @@ -899,7 +903,7 @@ func (g *GiteaLocalUploader) CreateReviews(ctx context.Context, reviews ...*base // SECURITY: The TreePath must be cleaned! use relative path comment.TreePath = util.PathJoinRel(comment.TreePath) - patch, _ := git.GetFileDiffCutAroundLine( + patch, _ := git.GetFileDiffCutAroundLine(ctx, g.gitRepo, pr.MergeBase, headCommitID, comment.TreePath, int64((&issues_model.Comment{Line: int64(line + comment.Position - 1)}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines, ) diff --git a/services/migrations/github.go b/services/migrations/github.go index b8bf6aee45..35ed7d7abd 100644 --- a/services/migrations/github.go +++ b/services/migrations/github.go @@ -20,7 +20,7 @@ import ( "gitea.dev/modules/proxy" "gitea.dev/modules/structs" - "github.com/google/go-github/v88/github" + "github.com/google/go-github/v89/github" "golang.org/x/oauth2" ) @@ -329,7 +329,7 @@ func (g *GithubDownloaderV3) convertGithubRelease(ctx context.Context, rel *gith r.Published = rel.PublishedAt.Time } - httpClient := NewMigrationHTTPClient() + httpClient := newMigrationHTTPClient() for _, asset := range rel.Assets { assetID := asset.GetID() // Don't optimize this, for closure we need a local variable TODO: no need to do so in new Golang diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go index cf4c2f24e9..585b4d8ed8 100644 --- a/services/migrations/gitlab.go +++ b/services/migrations/gitlab.go @@ -94,7 +94,7 @@ type GitlabDownloader struct { // Use either a username/password, personal token entered into the username field, or anonymous/public access // Note: Public access only allows very basic access func NewGitlabDownloader(ctx context.Context, baseURL, repoPath, token string) (*GitlabDownloader, error) { - gitlabClient, err := gitlab.NewClient(token, gitlab.WithBaseURL(baseURL), gitlab.WithHTTPClient(NewMigrationHTTPClient())) + gitlabClient, err := gitlab.NewClient(token, gitlab.WithBaseURL(baseURL), gitlab.WithHTTPClient(newMigrationHTTPClient())) if err != nil { log.Trace("Error logging into gitlab: %v", err) return nil, err @@ -314,7 +314,7 @@ func (g *GitlabDownloader) convertGitlabRelease(ctx context.Context, rel *gitlab PublisherName: rel.Author.Username, } - httpClient := NewMigrationHTTPClient() + httpClient := newMigrationHTTPClient() for _, asset := range rel.Assets.Links { assetID := asset.ID // Don't optimize this, for closure we need a local variable diff --git a/services/migrations/http_client.go b/services/migrations/http_client.go index 0d8b898a10..6fe7440c55 100644 --- a/services/migrations/http_client.go +++ b/services/migrations/http_client.go @@ -10,20 +10,33 @@ import ( "gitea.dev/modules/hostmatcher" "gitea.dev/modules/proxy" "gitea.dev/modules/setting" + "gitea.dev/modules/util" ) -// NewMigrationHTTPClient returns a HTTP client for migration -func NewMigrationHTTPClient() *http.Client { +// migrationHTTPClient is the shared migration client. Callers that would otherwise build a client per +// request use it (via getMigrationHTTPClient) so a single connection pool is reused across downloads — +// e.g. many release assets from the same host — instead of a fresh pool and TLS handshake each time. It +// is built lazily on first use and reset by Init whenever the allow/block lists change; OnceValue keeps +// concurrent callers sharing a single client instead of racing to create their own. +var migrationHTTPClient = util.OnceValue[*http.Client]{Func: newMigrationHTTPClient} + +// newMigrationHTTPClient returns a HTTP client for migration +func newMigrationHTTPClient() *http.Client { return &http.Client{ Transport: NewMigrationHTTPTransport(), } } -// NewMigrationHTTPTransport returns a HTTP transport for migration +// getMigrationHTTPClient returns the shared migration client, building it on first use so no request +// escapes the SSRF-validated transport even before Init has run. +func getMigrationHTTPClient() *http.Client { + return migrationHTTPClient.Value() +} + +// NewMigrationHTTPTransport returns a HTTP transport for migration. The target is validated against the +// allow/block lists on both the direct-dial and proxy paths, so a configured proxy cannot be used to +// reach an otherwise-forbidden target (SSRF). func NewMigrationHTTPTransport() *http.Transport { - return &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Migrations.SkipTLSVerify}, - Proxy: proxy.Proxy(), - DialContext: hostmatcher.NewDialContext("migration", allowList, blockList, setting.Proxy.ProxyURLFixed), - } + return hostmatcher.NewHTTPTransport("migration", allowList, blockList, proxy.Proxy(), setting.Proxy.ProxyURLFixed, + &tls.Config{InsecureSkipVerify: setting.Migrations.SkipTLSVerify}) } diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go index 90b98b531b..b1768f04ed 100644 --- a/services/migrations/migrate.go +++ b/services/migrations/migrate.go @@ -87,15 +87,14 @@ func IsMigrateURLAllowed(remoteURL string, doer *user_model.User) error { } func checkByAllowBlockList(hostName string, addrList []net.IP) error { - var ipAllowed bool + ipAllowed := len(addrList) > 0 var ipBlocked bool for _, addr := range addrList { - ipAllowed = ipAllowed || allowList.MatchIPAddr(addr) + ipAllowed = ipAllowed && allowList.MatchIPAddr(addr) ipBlocked = ipBlocked || blockList.MatchIPAddr(addr) } - var blockedError error if blockList.MatchHostName(hostName) || ipBlocked { - blockedError = &git.ErrInvalidCloneAddr{Host: hostName, IsPermissionDenied: true} + return &git.ErrInvalidCloneAddr{Host: hostName, IsPermissionDenied: true} } // if we have an allow-list, check the allow-list before return to get the more accurate error if !allowList.IsEmpty() { @@ -104,7 +103,7 @@ func checkByAllowBlockList(hostName string, addrList []net.IP) error { } } // otherwise, we always follow the blocked list - return blockedError + return nil } // MigrateRepository migrate repository according MigrateOptions @@ -524,9 +523,14 @@ func Init() error { if setting.Migrations.AllowLocalNetworks { allowList.AppendBuiltin(hostmatcher.MatchBuiltinPrivate) allowList.AppendBuiltin(hostmatcher.MatchBuiltinLoopback) + } else { + blockList.AppendBuiltin(hostmatcher.MatchBuiltinPrivate) + blockList.AppendBuiltin(hostmatcher.MatchBuiltinLoopback) } - // TODO: at the moment, if ALLOW_LOCALNETWORKS=false, ALLOWED_DOMAINS=domain.com, and domain.com has IP 127.0.0.1, then it's still allowed. - // if we want to block such case, the private&loopback should be added to the blockList when ALLOW_LOCALNETWORKS=false + + // reset the shared client so it is rebuilt from the freshly parsed lists on next use; download paths + // then reuse one connection pool instead of creating a client (and pool) per request + migrationHTTPClient.Reset() return nil } diff --git a/services/migrations/migrate_test.go b/services/migrations/migrate_test.go index 7c95a488ba..6903224bf3 100644 --- a/services/migrations/migrate_test.go +++ b/services/migrations/migrate_test.go @@ -93,17 +93,19 @@ func TestAllowBlockList(t *testing.T) { assert.NoError(t, checkByAllowBlockList("domain.com", []net.IP{net.ParseIP("1.2.3.4")})) assert.NoError(t, checkByAllowBlockList("domain.com", []net.IP{net.ParseIP("127.0.0.1")})) - // allow wildcard, block some subdomains. if the domain name is allowed, then the local network check is skipped + // allow wildcard, block some subdomains. every resolved address must still be allowed. init("*.domain.com", "blocked.domain.com", false) assert.NoError(t, checkByAllowBlockList("sub.domain.com", []net.IP{net.ParseIP("1.2.3.4")})) - assert.NoError(t, checkByAllowBlockList("sub.domain.com", []net.IP{net.ParseIP("127.0.0.1")})) + assert.Error(t, checkByAllowBlockList("sub.domain.com", []net.IP{net.ParseIP("127.0.0.1")})) + assert.Error(t, checkByAllowBlockList("sub.domain.com", []net.IP{net.ParseIP("1.2.3.4"), net.ParseIP("127.0.0.1")})) assert.Error(t, checkByAllowBlockList("blocked.domain.com", []net.IP{net.ParseIP("1.2.3.4")})) assert.Error(t, checkByAllowBlockList("sub.other.com", []net.IP{net.ParseIP("1.2.3.4")})) - // allow wildcard (it could lead to SSRF in production) + // allow wildcard still follows the local network policy for resolved addresses. init("*", "", false) assert.NoError(t, checkByAllowBlockList("domain.com", []net.IP{net.ParseIP("1.2.3.4")})) - assert.NoError(t, checkByAllowBlockList("domain.com", []net.IP{net.ParseIP("127.0.0.1")})) + assert.Error(t, checkByAllowBlockList("domain.com", []net.IP{net.ParseIP("127.0.0.1")})) + assert.Error(t, checkByAllowBlockList("domain.com", []net.IP{net.ParseIP("1.2.3.4"), net.ParseIP("127.0.0.1")})) // local network can still be blocked init("*", "127.0.0.*", false) diff --git a/services/migrations/restore.go b/services/migrations/restore.go index cb34f68186..a07c4d96b5 100644 --- a/services/migrations/restore.go +++ b/services/migrations/restore.go @@ -11,6 +11,7 @@ import ( "strconv" base "gitea.dev/modules/migration" + "gitea.dev/modules/util" "go.yaml.in/yaml/v4" ) @@ -144,7 +145,7 @@ func (r *RepositoryRestorer) GetReleases(_ context.Context) ([]*base.Release, er for _, rel := range releases { for _, asset := range rel.Assets { if asset.DownloadURL != nil { - *asset.DownloadURL = "file://" + filepath.Join(r.baseDir, *asset.DownloadURL) + *asset.DownloadURL = "file://" + util.FilePathJoinAbs(r.baseDir, *asset.DownloadURL) } } } @@ -235,8 +236,10 @@ func (r *RepositoryRestorer) GetPullRequests(_ context.Context, page, perPage in return nil, false, err } for _, pr := range pulls { - pr.PatchURL = "file://" + filepath.Join(r.baseDir, pr.PatchURL) - CheckAndEnsureSafePR(pr, "", r) + if pr.PatchURL != "" { + pr.PatchURL = "file://" + util.FilePathJoinAbs(r.baseDir, pr.PatchURL) + } + CheckAndEnsureSafePR(pr, "file://"+r.baseDir, r) } return pulls, true, nil } diff --git a/services/migrations/restore_test.go b/services/migrations/restore_test.go new file mode 100644 index 0000000000..21cba5e349 --- /dev/null +++ b/services/migrations/restore_test.go @@ -0,0 +1,74 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package migrations + +import ( + "os" + "path/filepath" + "testing" + + "gitea.dev/modules/optional" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestRepositoryRestorer_GetReleases_LocalFileInclusion ensures a crafted +// release.yml cannot make the restorer read files outside the dump +// directory via a path-traversal DownloadURL. +func TestRepositoryRestorer_GetReleases_LocalFileInclusion(t *testing.T) { + baseDir := t.TempDir() + + // a legitimate attachment that lives inside the dump directory + require.NoError(t, os.WriteFile(filepath.Join(baseDir, "good.txt"), []byte("ok"), 0o644)) + + releaseYML := ` +- assets: + - name: good.txt + download_url: good.txt + - name: evil.txt + download_url: ../../../../../../../../etc/passwd +` + require.NoError(t, os.WriteFile(filepath.Join(baseDir, "release.yml"), []byte(releaseYML), 0o644)) + + r, err := NewRepositoryRestorer(t.Context(), baseDir, "owner", "repo", false) + require.NoError(t, err) + + releases, err := r.GetReleases(t.Context()) + require.NoError(t, err) + require.Len(t, releases, 1) + require.Len(t, releases[0].Assets, 2) + + // the in-dump asset keeps a file:// URL pointing inside baseDir + assets := releases[0].Assets + assert.Equal(t, "file://"+filepath.Join(baseDir, "good.txt"), optional.FromPtr(assets[0].DownloadURL).Value()) + assert.Equal(t, "file://"+filepath.Join(baseDir, "etc/passwd"), optional.FromPtr(assets[1].DownloadURL).Value()) +} + +func TestRepositoryRestorer_GetPullRequestsStripsUnsafeCloneURL(t *testing.T) { + baseDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(baseDir, "change.patch"), []byte("patch"), 0o644)) + + pullRequestYML := ` +- number: 1 + patch_url: change.patch + head: + clone_url: http://127.0.0.1/private.git + ref: feature + base: + ref: main +` + require.NoError(t, os.WriteFile(filepath.Join(baseDir, "pull_request.yml"), []byte(pullRequestYML), 0o644)) + + r, err := NewRepositoryRestorer(t.Context(), baseDir, "owner", "repo", false) + require.NoError(t, err) + + pulls, _, err := r.GetPullRequests(t.Context(), 1, 10) + require.NoError(t, err) + require.Len(t, pulls, 1) + + assert.Equal(t, "file://"+filepath.Join(baseDir, "change.patch"), pulls[0].PatchURL) + assert.Empty(t, pulls[0].Head.CloneURL) + assert.True(t, pulls[0].EnsuredSafe) +} diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 5a9e546d56..fa8bf0e94c 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -70,16 +70,17 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error return repo_model.UpdateRepositoryColsNoAutoTime(ctx, m.Repo, "original_url") } -func pruneBrokenReferences(ctx context.Context, m *repo_model.Mirror, gitRepo gitrepo.Repository, timeout time.Duration) error { - cmd := gitcmd.NewCommand("remote", "prune").AddDynamicArguments(m.GetRemoteName()).WithTimeout(timeout) +func pruneBrokenReferences(ctx context.Context, m *repo_model.Mirror, repoLogName string, gitRepo gitrepo.Repository, timeout time.Duration) error { + // Never follow HTTP redirects, see cmdFetch in runSync. + cmd := gitcmd.NewCommand("remote", "prune").AddConfig("http.followRedirects", "false").AddDynamicArguments(m.GetRemoteName()).WithTimeout(timeout) stdout, _, pruneErr := gitrepo.RunCmdString(ctx, gitRepo, cmd) if pruneErr != nil { // sanitize the output, since it may contain the remote address, which may contain a password stderrMessage := util.SanitizeCredentialURLs(pruneErr.Stderr()) stdoutMessage := util.SanitizeCredentialURLs(stdout) - log.Error("Failed to prune mirror repository %s references:\nStdout: %s\nStderr: %s\nErr: %v", gitRepo.RelativePath(), stdoutMessage, stderrMessage, pruneErr) - desc := fmt.Sprintf("Failed to prune mirror repository (%s) references: %s", m.Repo.FullName(), stderrMessage) + log.Error("Failed to prune mirror repository %s references:\nStdout: %s\nStderr: %s\nErr: %v", repoLogName, stdoutMessage, stderrMessage, pruneErr) + desc := fmt.Sprintf("Failed to prune mirror repository (%s) references: %s", repoLogName, stderrMessage) if err := system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } @@ -114,12 +115,23 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*repo_module.SyncResu log.Error("SyncMirrors [repo: %-v]: GetRemoteURL Error %v", m.Repo, remoteErr) return nil, false } + // re-validate on every sync: the host may now resolve to an internal IP (rebinding) or the + // allow/block list may have changed. ssh/file are skipped (not an HTTP SSRF vector). + switch remoteURL.URL.Scheme { + case "http", "https", "git": + if allowErr := migrations.IsMigrateURLAllowed(remoteURL.String(), m.Repo.MustOwner(ctx)); allowErr != nil { + log.Error("SyncMirrors [repo: %-v]: remote URL is not allowed: %v", m.Repo, allowErr) + return nil, false + } + } envs := proxy.EnvWithProxy(remoteURL.URL) timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second // use fetch but not remote update because git fetch support --tags but remote update doesn't cmdFetch := func() *gitcmd.Command { - cmd := gitcmd.NewCommand("fetch", "--tags") + // Never follow HTTP redirects: a mirror remote that later starts redirecting to an + // otherwise-blocked address would be an SSRF/exfiltration vector on scheduled syncs. + cmd := gitcmd.NewCommand("fetch", "--tags").AddConfig("http.followRedirects", "false") if m.EnablePrune { cmd.AddArguments("--prune") } @@ -138,7 +150,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*repo_module.SyncResu log.Warn("SyncMirrors [repo: %-v]: failed to update mirror repository due to broken references:\nStdout: %s\nStderr: %s\nErr: %v\nAttempting Prune", m.Repo, stdoutMessage, stderrMessage, err) err = nil // Attempt prune - pruneErr := pruneBrokenReferences(ctx, m, m.Repo, timeout) + pruneErr := pruneBrokenReferences(ctx, m, m.Repo.FullName(), m.Repo, timeout) if pruneErr == nil { // Successful prune - reattempt mirror fetchStdout, fetchStderr, err = gitrepo.RunCmdString(ctx, m.Repo, cmdFetch()) @@ -160,11 +172,11 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*repo_module.SyncResu return nil, false } } - if err := gitrepo.WriteCommitGraph(ctx, m.Repo); err != nil { + if err := git.WriteCommitGraph(ctx, m.Repo); err != nil { log.Error("SyncMirrors [repo: %-v]: %v", m.Repo, err) } - gitRepo, err := gitrepo.OpenRepository(ctx, m.Repo) + gitRepo, err := gitrepo.OpenRepository(m.Repo) if err != nil { log.Error("SyncMirrors [repo: %-v]: failed to OpenRepository: %v", m.Repo, err) return nil, false @@ -200,7 +212,8 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*repo_module.SyncResu } cmdRemoteUpdatePrune := func() *gitcmd.Command { - return gitcmd.NewCommand("remote", "update", "--prune"). + // Never follow HTTP redirects, see cmdFetch above. + return gitcmd.NewCommand("remote", "update", "--prune").AddConfig("http.followRedirects", "false"). AddDynamicArguments(m.GetRemoteName()).WithTimeout(timeout).WithEnv(envs) } @@ -219,7 +232,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*repo_module.SyncResu err = nil // Attempt prune - pruneErr := pruneBrokenReferences(ctx, m, m.Repo.WikiStorageRepo(), timeout) + pruneErr := pruneBrokenReferences(ctx, m, m.Repo.FullName()+".wiki", m.Repo.WikiStorageRepo(), timeout) if pruneErr == nil { // Successful prune - reattempt mirror stdout, stderr, err = gitrepo.RunCmdString(ctx, m.Repo.WikiStorageRepo(), cmdRemoteUpdatePrune()) @@ -240,7 +253,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*repo_module.SyncResu return nil, false } - if err := gitrepo.WriteCommitGraph(ctx, m.Repo.WikiStorageRepo()); err != nil { + if err := git.WriteCommitGraph(ctx, m.Repo.WikiStorageRepo()); err != nil { log.Error("SyncMirrors [repo: %-v]: %v", m.Repo, err) } } @@ -312,7 +325,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool { return false } - gitRepo, err := gitrepo.OpenRepository(ctx, m.Repo) + gitRepo, err := gitrepo.OpenRepository(m.Repo) if err != nil { log.Error("SyncMirrors [repo: %-v]: unable to OpenRepository: %v", m.Repo, err) return false @@ -335,7 +348,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool { // Create reference if result.OldCommitID == "" { - commitID, err := gitRepo.GetRefCommitID(result.RefName.String()) + commitID, err := gitRepo.GetRefCommitID(ctx, result.RefName.String()) if err != nil { log.Error("SyncMirrors [repo: %-v]: unable to GetRefCommitID [ref_name: %s]: %v", m.Repo, result.RefName, err) continue @@ -357,14 +370,14 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool { } oldCommitID, newCommitID := result.OldCommitID, result.NewCommitID - commits, err := gitRepo.CommitsBetween(newCommitID, oldCommitID, setting.UI.FeedMaxCommitNum) + commits, err := gitRepo.CommitsBetween(ctx, newCommitID, oldCommitID, setting.UI.FeedMaxCommitNum) if err != nil { log.Error("SyncMirrors [repo: %-v]: unable to get CommitsBetween [new_commit_id: %s, old_commit_id: %s]: %v", m.Repo, newCommitID, oldCommitID, err) continue } theCommits := repo_module.GitToPushCommits(commits) - newCommit, err := gitRepo.GetCommit(newCommitID.String()) + newCommit, err := gitRepo.GetCommit(ctx, newCommitID.String()) if err != nil { log.Error("SyncMirrors [repo: %-v]: unable to get commit %s: %v", m.Repo, newCommitID, err) continue @@ -381,7 +394,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool { } log.Trace("SyncMirrors [repo: %-v]: done notifying updated branches/tags - now updating last commit time", m.Repo) - isEmpty, err := gitRepo.IsEmpty() + isEmpty, err := gitRepo.IsEmpty(ctx) if err != nil { log.Error("SyncMirrors [repo: %-v]: unable to check empty git repo: %v", m.Repo, err) return false diff --git a/services/mirror/mirror_push.go b/services/mirror/mirror_push.go index 5df0c8a066..6df5a5d701 100644 --- a/services/mirror/mirror_push.go +++ b/services/mirror/mirror_push.go @@ -128,19 +128,20 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { if isWiki { storageRepo = repo.WikiStorageRepo() } + mirrorLogName := fmt.Sprintf("%s%s[mirror=%d]", m.Repo.FullName(), util.Iif(isWiki, ".wiki", ""), m.ID) remoteURL, err := gitrepo.GitRemoteGetURL(ctx, storageRepo, m.RemoteName) if err != nil { - log.Error("GetRemoteURL(%s) Error %v", storageRepo.RelativePath(), err) - return errors.New("Unexpected error") + log.Error("GetRemoteURL %s failed, error %v", mirrorLogName, err) + return errors.New("GitRemoteGetURL failed") } if setting.LFS.StartServer { log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo) - gitRepo, err := gitrepo.OpenRepository(ctx, storageRepo) + gitRepo, err := gitrepo.OpenRepository(storageRepo) if err != nil { - log.Error("OpenRepository: %v", err) - return errors.New("Unexpected error") + log.Error("OpenRepository %s failed: %v", mirrorLogName, err) + return errors.New("OpenRepository failed") } defer gitRepo.Close() @@ -153,7 +154,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { } } - log.Trace("Pushing %s mirror[%d] remote %s", storageRepo.RelativePath(), m.ID, m.RemoteName) + log.Trace("Pushing %s remote %s", mirrorLogName, m.ID, m.RemoteName) envs := proxy.EnvWithProxy(remoteURL.URL) if err := gitrepo.PushToExternal(ctx, storageRepo, git.PushOptions{ @@ -163,8 +164,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { Timeout: timeout, Env: envs, }); err != nil { - log.Error("Error pushing %s mirror[%d] remote %s: %v", storageRepo.RelativePath(), m.ID, m.RemoteName, err) - + log.Error("Error pushing %s remote %s: %v", mirrorLogName, m.RemoteName, err) return util.SanitizeErrorCredentialURLs(err) } diff --git a/services/org/org.go b/services/org/org.go index 2f2ee95031..7ab8493aa4 100644 --- a/services/org/org.go +++ b/services/org/org.go @@ -39,6 +39,7 @@ func deleteOrganization(ctx context.Context, org *org_model.Organization) error &user_model.Blocking{BlockerID: org.ID}, &actions_model.ActionRunner{OwnerID: org.ID}, &actions_model.ActionRunnerToken{OwnerID: org.ID}, + &actions_model.ActionScopedWorkflowSource{OwnerID: org.ID}, ); err != nil { return fmt.Errorf("DeleteBeans: %w", err) } @@ -119,9 +120,14 @@ func updateRepoForVisibilityChanged(ctx context.Context, repo *repo_model.Reposi return err } + // the repo is no longer publicly visible, so drop stars and watches from users who can no longer + // see it, matching the direct repository-private transition (see services/repository) if err := repo_model.ClearRepoStars(ctx, repo.ID); err != nil { return err } + if err := repo_model.ClearRepoWatches(ctx, repo.ID); err != nil { + return err + } } // Create/Remove git-daemon-export-ok for git-daemon... diff --git a/services/org/org_test.go b/services/org/org_test.go index 0d6f4f2489..2f30db327a 100644 --- a/services/org/org_test.go +++ b/services/org/org_test.go @@ -68,4 +68,23 @@ func TestOrg(t *testing.T) { require.NoError(t, ChangeOrganizationVisibility(t.Context(), org, structs.VisibleTypePrivate)) unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: org.ID, Visibility: structs.VisibleTypePrivate}) }) + + t.Run("ChangeVisibilityClearsWatchesAndStars", func(t *testing.T) { + // org3 is a public organization owning the public repo32 + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + require.Equal(t, structs.VisibleTypePublic, org.Visibility) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32, OwnerID: org.ID}) + + // an outside user watches and stars the repo while the org is still visible + watcher := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + require.NoError(t, repo_model.WatchRepo(t.Context(), watcher, repo, true)) + require.NoError(t, repo_model.StarRepo(t.Context(), watcher, repo, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{UserID: watcher.ID, RepoID: repo.ID}) + + require.NoError(t, ChangeOrganizationVisibility(t.Context(), org, structs.VisibleTypePrivate)) + + // making the org private must drop watches, not only stars, from users who can no longer see it + unittest.AssertNotExistsBean(t, &repo_model.Watch{UserID: watcher.ID, RepoID: repo.ID}) + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: watcher.ID, RepoID: repo.ID}) + }) } diff --git a/services/packages/cargo/index.go b/services/packages/cargo/index.go index dd95e3001b..9bab40de36 100644 --- a/services/packages/cargo/index.go +++ b/services/packages/cargo/index.go @@ -278,7 +278,7 @@ func alterRepositoryContent(ctx context.Context, doer *user_model.User, repo *re return err } - commit, err := t.GetBranchCommit(repo.DefaultBranch) + commit, err := t.GetBranchCommit(ctx, repo.DefaultBranch) if err != nil { return err } diff --git a/services/projects/issue.go b/services/projects/issue.go index 01b263760b..ad9abe9d4a 100644 --- a/services/projects/issue.go +++ b/services/projects/issue.go @@ -13,8 +13,9 @@ import ( issues_model "gitea.dev/models/issues" project_model "gitea.dev/models/project" user_model "gitea.dev/models/user" - "gitea.dev/modules/container" "gitea.dev/modules/optional" + + "xorm.io/builder" ) // MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column @@ -100,28 +101,15 @@ func MoveIssuesOnProjectColumn(ctx context.Context, doer *user_model.User, colum }) } -func LoadIssuesAssigneesForProject(ctx context.Context, issuesMap map[int64]issues_model.IssueList) ([]*user_model.User, error) { - var issueList issues_model.IssueList - for _, colIssues := range issuesMap { - issueList = append(issueList, colIssues...) - } - err := issueList.LoadAssignees(ctx) +func LoadIssuesAssigneesForProject(ctx context.Context, projectID int64) (users []*user_model.User, _ error) { + sub := builder.Select("distinct issue_assignees.assignee_id"). + From("project_issue").Join("INNER", "issue_assignees", "project_issue.issue_id=issue_assignees.issue_id"). + Where(builder.Eq{"project_issue.project_id": projectID}) + err := db.GetEngine(ctx).Table("`user`").Where(builder.In("id", sub)).Find(&users) if err != nil { return nil, err } - users := make([]*user_model.User, 0, len(issueList)) - usersAdded := container.Set[int64]{} - for _, issue := range issueList { - for _, assignee := range issue.Assignees { - if !usersAdded.Contains(assignee.ID) { - usersAdded.Add(assignee.ID) - users = append(users, assignee) - } - } - } - slices.SortFunc(users, func(a, b *user_model.User) int { - return strings.Compare(a.Name, b.Name) - }) + slices.SortFunc(users, func(a, b *user_model.User) int { return strings.Compare(a.Name, b.Name) }) return users, nil } diff --git a/services/projects/issue_test.go b/services/projects/issue_test.go index ac0ed2002d..7f353ca5b2 100644 --- a/services/projects/issue_test.go +++ b/services/projects/issue_test.go @@ -15,7 +15,6 @@ import ( user_model "gitea.dev/models/user" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func Test_Projects(t *testing.T) { @@ -25,6 +24,9 @@ func Test_Projects(t *testing.T) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) org3 := unittest.AssertExistsAndLoadBean(t, &org_model.Organization{ID: 3}) user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + // user15 is on org3's team7 (write access to the public repo32 only), so it can see org3 public repos + // but has no access to the private repo3 — a genuine "no permission to the private repo" org member. + user15 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}) t.Run("User projects", func(t *testing.T) { pi1 := project_model.ProjectIssue{ @@ -143,13 +145,27 @@ func Test_Projects(t *testing.T) { }) t.Run("Authenticated user with no permission to the private repo", func(t *testing.T) { + // user2 is on org3's Owners team and has owner access to the private repo3, so it is not a + // valid "no permission" subject; user15 has no access to repo3 but can see the public repo32. + columnIssues, err := LoadIssuesFromProject(t.Context(), projects[0], &issues_model.IssuesOptions{ + Owner: org3.AsUser(), + Doer: user15, + }) + assert.NoError(t, err) + assert.Len(t, columnIssues, 1) + assert.Len(t, columnIssues[defaultColumn.ID], 1) // user15 can only visit public repo issues + }) + + t.Run("Org owner team member", func(t *testing.T) { + // user2 is on org3's Owners team, so it has access to the private repo3 and must see both the + // public and the private issue — the owner-team access that team.authorize grants at runtime. columnIssues, err := LoadIssuesFromProject(t.Context(), projects[0], &issues_model.IssuesOptions{ Owner: org3.AsUser(), Doer: user2, }) assert.NoError(t, err) assert.Len(t, columnIssues, 1) - assert.Len(t, columnIssues[defaultColumn.ID], 1) // user2 can only visit public repo issues + assert.Len(t, columnIssues[defaultColumn.ID], 2) // owner-team member visits both public and private issues }) }) @@ -198,18 +214,4 @@ func Test_Projects(t *testing.T) { assert.Len(t, columnIssues[3], 1) }) }) - - t.Run("LoadIssuesAssigneesForProject", func(t *testing.T) { - issuesMap := map[int64]issues_model.IssueList{} - issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - issue6 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6}) - issuesMap[1] = issues_model.IssueList{issue1} - issuesMap[2] = issues_model.IssueList{issue6} - assignees, err := LoadIssuesAssigneesForProject(t.Context(), issuesMap) - require.NoError(t, err) - require.Len(t, assignees, 3) - require.Equal(t, "user1", assignees[0].Name) - require.Equal(t, "user10", assignees[1].Name) - require.Equal(t, "user2", assignees[2].Name) - }) } diff --git a/services/pull/check.go b/services/pull/check.go index 92021b7d1c..64b252b066 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -264,7 +264,7 @@ func checkSigningRequirements(ctx context.Context, pr *issues_model.PullRequest, } if mergeStyle != repo_model.MergeStyleFastForwardOnly { - if _, _, _, err := asymkey_service.SignMerge(ctx, pr, doer, gitRepo); err != nil { + if _, _, _, err := asymkey_service.SignMerge(ctx, pr, doer, gitRepo, pr.BaseBranch, pr.GetGitHeadRefName()); err != nil { return err } } @@ -333,7 +333,7 @@ func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Com return nil, fmt.Errorf("GetFullCommitID(%s) in %s: %w", prHeadRef, pr.BaseRepo.FullName(), err) } - gitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo) + gitRepo, err := gitrepo.OpenRepository(pr.BaseRepo) if err != nil { return nil, fmt.Errorf("%-v OpenRepository: %w", pr.BaseRepo, err) } @@ -360,7 +360,7 @@ func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Com // PR was maybe fast-forwarded, so just use last commit of PR mergeCommit = prHeadCommitID } - commit, err := gitRepo.GetCommit(mergeCommit) + commit, err := gitRepo.GetCommit(ctx, mergeCommit) if err != nil { return nil, fmt.Errorf("GetMergeCommit[%s]: %w", mergeCommit, err) } diff --git a/services/pull/comment.go b/services/pull/comment.go index 7cbd84b7d8..859c8b8b04 100644 --- a/services/pull/comment.go +++ b/services/pull/comment.go @@ -106,12 +106,12 @@ func CreatePushPullComment(ctx context.Context, pusher *user_model.User, pr *iss oldCommitID := oldRef if !git.IsEmptyCommitID(oldRef) { - oldCommitID, err = gitRepo.GetRefCommitID(oldRef) + oldCommitID, err = gitRepo.GetRefCommitID(ctx, oldRef) if err != nil { return nil, false, err } } - newCommitID, err := gitRepo.GetRefCommitID(newRef) + newCommitID, err := gitRepo.GetRefCommitID(ctx, newRef) if err != nil { return nil, false, err } diff --git a/services/pull/comment_test.go b/services/pull/comment_test.go index 3d5b995e53..5836575fdc 100644 --- a/services/pull/comment_test.go +++ b/services/pull/comment_test.go @@ -19,13 +19,14 @@ import ( ) func TestCreatePushPullCommentForcePushDeletesOldComments(t *testing.T) { + ctx := t.Context() require.NoError(t, unittest.PrepareTestDatabase()) pusher := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) - require.NoError(t, pr.LoadIssue(t.Context())) - require.NoError(t, pr.LoadBaseRepo(t.Context())) + require.NoError(t, pr.LoadIssue(ctx)) + require.NoError(t, pr.LoadBaseRepo(ctx)) - gitRepo, err := gitrepo.OpenRepository(t.Context(), pr.BaseRepo) + gitRepo, err := gitrepo.OpenRepository(pr.BaseRepo) require.NoError(t, err) defer gitRepo.Close() @@ -65,7 +66,7 @@ func TestCreatePushPullCommentForcePushDeletesOldComments(t *testing.T) { insertCommitComment(t, issues_model.PushActionContent{}) assertCommitCommentCount(t, 2, 0) - baseCommit, err := gitRepo.GetBranchCommit(pr.BaseBranch) + baseCommit, err := gitRepo.GetBranchCommit(ctx, pr.BaseBranch) assert.NoError(t, err) // force push, the old push comments should be deleted, and one new force-push comment should be created. @@ -83,7 +84,7 @@ func TestCreatePushPullCommentForcePushDeletesOldComments(t *testing.T) { t.Run("force-push-ignores-missing-old-commit", func(t *testing.T) { require.NoError(t, db.TruncateBeans(t.Context(), &issues_model.Comment{})) - headCommit, err := gitRepo.GetBranchCommit(pr.HeadBranch) + headCommit, err := gitRepo.GetBranchCommit(ctx, pr.HeadBranch) require.NoError(t, err) commitIDZero := git.Sha1ObjectFormat.EmptyObjectID().String() @@ -110,9 +111,9 @@ func TestCreatePushPullCommentForcePushDeletesOldComments(t *testing.T) { insertCommitComment(t, issues_model.PushActionContent{}) assertCommitCommentCount(t, 4, 0) - baseCommit, err := gitRepo.GetBranchCommit(pr.BaseBranch) + baseCommit, err := gitRepo.GetBranchCommit(ctx, pr.BaseBranch) require.NoError(t, err) - headCommit, err := gitRepo.GetBranchCommit(pr.HeadBranch) + headCommit, err := gitRepo.GetBranchCommit(ctx, pr.HeadBranch) require.NoError(t, err) _, _, err = CreatePushPullComment(t.Context(), pusher, pr, baseCommit.ID.String(), headCommit.ID.String(), true) diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go index 6fafcf3b51..fb6ba7f9db 100644 --- a/services/pull/commit_status.go +++ b/services/pull/commit_status.go @@ -8,11 +8,15 @@ import ( "context" "errors" "fmt" + "slices" + actions_model "gitea.dev/models/actions" "gitea.dev/models/db" git_model "gitea.dev/models/git" issues_model "gitea.dev/models/issues" + repo_model "gitea.dev/models/repo" "gitea.dev/modules/commitstatus" + "gitea.dev/modules/container" "gitea.dev/modules/gitrepo" "gitea.dev/modules/glob" "gitea.dev/modules/log" @@ -69,11 +73,25 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus, func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (bool, error) { pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) if err != nil { - return false, fmt.Errorf("GetLatestCommitStatus: %w", err) + return false, fmt.Errorf("GetFirstMatchProtectedBranchRule: %w", err) } - if pb == nil || !pb.EnableStatusCheck { + if pb == nil { return true, nil } + if !pb.EnableStatusCheck { + // The branch's own status check is off, but required scoped checks (mandated by the owner or instance admin) still gate the merge. + if err := pr.LoadBaseRepo(ctx); err != nil { + return false, err + } + required, err := EffectiveRequiredContexts(ctx, pr.BaseRepo, pb) + if err != nil { + return false, err + } + if len(required) == 0 { + // With none in effect there is nothing to enforce, so don't block + return true, nil + } + } state, err := GetPullRequestCommitStatusState(ctx, pr) if err != nil { @@ -109,9 +127,9 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullR var sha string if pr.Flow == issues_model.PullRequestFlowGithub { - sha, err = headGitRepo.GetBranchCommitID(pr.HeadBranch) + sha, err = headGitRepo.GetBranchCommitID(ctx, pr.HeadBranch) } else { - sha, err = headGitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + sha, err = headGitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) } if err != nil { return "", err @@ -130,10 +148,54 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullR if err != nil { return "", fmt.Errorf("LoadProtectedBranch: %w", err) } - var requiredContexts []string - if pb != nil { - requiredContexts = pb.StatusCheckContexts + requiredContexts, err := EffectiveRequiredContexts(ctx, pr.BaseRepo, pb) + if err != nil { + return "", err } return MergeRequiredContextsCommitStatus(commitStatuses, requiredContexts), nil } + +// EffectiveRequiredContexts returns the required status-check contexts to enforce, drawn from: +// 1. every required scoped workflow's status-check patterns effective for the repo (always) +// 2. each given protected branch rule's own configured contexts, only when that rule's status check is enabled +// +// Passing no rule or a single nil rule yields nothing, not even scoped patterns. +// A single rule yields that rule's effective contexts. +// Passing several rules unions their effective contexts; this is used when the governing rule is not yet known. +func EffectiveRequiredContexts(ctx context.Context, repo *repo_model.Repository, pbs ...*git_model.ProtectedBranch) ([]string, error) { + // No protection rule in effect: nothing is required. + if len(pbs) == 0 || (len(pbs) == 1 && pbs[0] == nil) { + return nil, nil + } + + sources, err := actions_model.GetEffectiveScopedWorkflowSources(ctx, repo.OwnerID) + if err != nil { + return nil, fmt.Errorf("GetEffectiveScopedWorkflowSources: %w", err) + } + + required := make(container.Set[string]) + + // Every required scoped workflow's admin-authored status-check patterns, matched must-present-and-pass: + // a required scoped check that posts no matching status blocks the merge. + for _, source := range sources { + for _, cfg := range source.WorkflowConfigs { + if !cfg.Required { + continue + } + required.AddMultiple(cfg.Patterns...) + } + } + + // Union the configured contexts of every rule whose own status check is enabled (a disabled rule contributes none) + for _, pb := range pbs { + if pb == nil || !pb.EnableStatusCheck { + continue + } + required.AddMultiple(pb.StatusCheckContexts...) + } + + values := required.Values() + slices.Sort(values) // stable output + return values, nil +} diff --git a/services/pull/commit_status_test.go b/services/pull/commit_status_test.go index 612ae268cb..20889a7626 100644 --- a/services/pull/commit_status_test.go +++ b/services/pull/commit_status_test.go @@ -7,10 +7,15 @@ package pull import ( "testing" + actions_model "gitea.dev/models/actions" + "gitea.dev/models/db" git_model "gitea.dev/models/git" + repo_model "gitea.dev/models/repo" + "gitea.dev/models/unittest" "gitea.dev/modules/commitstatus" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMergeRequiredContextsCommitStatus(t *testing.T) { @@ -90,3 +95,72 @@ func TestMergeRequiredContextsCommitStatus(t *testing.T) { assert.Equal(t, c.expected, MergeRequiredContextsCommitStatus(c.commitStatuses, c.requiredContexts), "case %d", i) } } + +// TestEffectiveRequiredContexts: every required scoped workflow's stored status-check patterns are appended to the +// branch protection's configured contexts unconditionally (must-present; the matching is done downstream). +func TestEffectiveRequiredContexts(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + consumer := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) // owned by user5 + pbOn := &git_model.ProtectedBranch{EnableStatusCheck: true, StatusCheckContexts: []string{"configured/check"}} + + t.Run("nil protected branch: nil", func(t *testing.T) { + got, err := EffectiveRequiredContexts(t.Context(), consumer, nil) + require.NoError(t, err) + assert.Nil(t, got) + }) + + t.Run("status checks disabled, no required scoped: nothing required", func(t *testing.T) { + pbOff := &git_model.ProtectedBranch{EnableStatusCheck: false, StatusCheckContexts: []string{"configured/check"}} + got, err := EffectiveRequiredContexts(t.Context(), consumer, pbOff) + require.NoError(t, err) + assert.Empty(t, got) // the rule's own status check is off and no required scoped workflow applies -> nothing gates + }) + + t.Run("owner with no scoped sources: configured contexts unchanged", func(t *testing.T) { + noSourceRepo := &repo_model.Repository{ID: consumer.ID, OwnerID: 99999} + got, err := EffectiveRequiredContexts(t.Context(), noSourceRepo, pbOn) + require.NoError(t, err) + assert.Equal(t, []string{"configured/check"}, got) + }) + + t.Run("required workflow patterns appended", func(t *testing.T) { + require.NoError(t, db.Insert(t.Context(), &actions_model.ActionScopedWorkflowSource{ + OwnerID: consumer.OwnerID, + SourceRepoID: 1, + WorkflowConfigs: map[string]*actions_model.ScopedWorkflowConfig{ + "ci.yaml": {Required: true, Patterns: []string{"org/src: ci.yaml / build (pull_request)", "org/src: ci.yaml / lint (pull_request)"}}, + "old.yaml": {Required: false, Patterns: []string{"org/src: old.yaml / *"}}, // kept as history, must NOT be enforced + }, + })) + // No status is passed/needed: required patterns are enforced even though nothing has posted them yet (must-present). + got, err := EffectiveRequiredContexts(t.Context(), consumer, pbOn) + require.NoError(t, err) + assert.ElementsMatch(t, []string{ + "configured/check", + "org/src: ci.yaml / build (pull_request)", + "org/src: ci.yaml / lint (pull_request)", + }, got) + assert.NotContains(t, got, "org/src: old.yaml / *", "a non-required (history) config must not be enforced") + }) + + t.Run("status checks disabled, with required scoped: only the scoped patterns gate", func(t *testing.T) { + pbOff := &git_model.ProtectedBranch{EnableStatusCheck: false, StatusCheckContexts: []string{"configured/check"}} + got, err := EffectiveRequiredContexts(t.Context(), consumer, pbOff) + require.NoError(t, err) + // "configured/check" is dropped (the rule's own status check is off); only the required scoped patterns remain. + assert.ElementsMatch(t, []string{ + "org/src: ci.yaml / build (pull_request)", + "org/src: ci.yaml / lint (pull_request)", + }, got) + }) + + t.Run("several rules union their enabled contexts, deduplicated; a disabled rule contributes none", func(t *testing.T) { + noSourceRepo := &repo_model.Repository{ID: consumer.ID, OwnerID: 99999} // no scoped sources, so only the rules' contexts gate + a := &git_model.ProtectedBranch{EnableStatusCheck: true, StatusCheckContexts: []string{"a/check", "shared/check"}} + b := &git_model.ProtectedBranch{EnableStatusCheck: true, StatusCheckContexts: []string{"b/check", "shared/check"}} + off := &git_model.ProtectedBranch{EnableStatusCheck: false, StatusCheckContexts: []string{"off/check"}} + got, err := EffectiveRequiredContexts(t.Context(), noSourceRepo, a, b, off) + require.NoError(t, err) + assert.ElementsMatch(t, []string{"a/check", "b/check", "shared/check"}, got) + }) +} diff --git a/services/pull/merge.go b/services/pull/merge.go index fe65c4d448..0c567c4628 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -68,11 +68,11 @@ func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issue if mergeStyle != "" { templateFilepath := fmt.Sprintf(".gitea/default_merge_message/%s_TEMPLATE.md", strings.ToUpper(string(mergeStyle))) - commit, err := baseGitRepo.GetBranchCommit(pr.BaseRepo.DefaultBranch) + commit, err := baseGitRepo.GetBranchCommit(ctx, pr.BaseRepo.DefaultBranch) if err != nil { return "", "", err } - templateContent, err := commit.GetFileContent(templateFilepath, setting.Repository.PullRequest.DefaultMergeMessageSize) + templateContent, err := commit.GetFileContent(ctx, baseGitRepo, templateFilepath, setting.Repository.PullRequest.DefaultMergeMessageSize) if err != nil { if !git.IsErrNotExist(err) { return "", "", err @@ -642,7 +642,7 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use return errors.New("Wrong commit ID") } - commit, err := baseGitRepo.GetCommit(commitID) + commit, err := baseGitRepo.GetCommit(ctx, commitID) if err != nil { if git.IsErrNotExist(err) { return errors.New("Wrong commit ID") @@ -651,7 +651,7 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use } commitID = commit.ID.String() - ok, err := baseGitRepo.IsCommitInBranch(commitID, pr.BaseBranch) + ok, err := baseGitRepo.IsCommitInBranch(ctx, commitID, pr.BaseBranch) if err != nil { return err } diff --git a/services/pull/merge_prepare.go b/services/pull/merge_prepare.go index 37f5593e5d..974250357d 100644 --- a/services/pull/merge_prepare.go +++ b/services/pull/merge_prepare.go @@ -18,7 +18,6 @@ import ( user_model "gitea.dev/models/user" "gitea.dev/modules/git" "gitea.dev/modules/git/gitcmd" - "gitea.dev/modules/gitrepo" "gitea.dev/modules/log" "gitea.dev/modules/util" asymkey_service "gitea.dev/services/asymkey" @@ -103,15 +102,18 @@ func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullReque mergeCtx.sig = doer.NewGitSig() mergeCtx.committer = mergeCtx.sig - gitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo) + gitRepo, err := git.OpenRepositoryLocal(mergeCtx.tmpBasePath) if err != nil { defer cancel() return nil, nil, fmt.Errorf("failed to open temp git repo for pr[%d]: %w", mergeCtx.pr.ID, err) } defer gitRepo.Close() - // Determine if we should sign - sign, key, signer, _ := asymkey_service.SignMerge(ctx, pr, doer, gitRepo) + // Determine if we should sign, using the temp repo's own refs (see SignMerge for why) + sign, key, signer, err := asymkey_service.SignMerge(ctx, pr, doer, gitRepo, git.BranchPrefix+tmpRepoBaseBranch, git.BranchPrefix+tmpRepoTrackingBranch) + if err != nil && !asymkey_service.IsErrWontSign(err) { + log.Error("%-v SignMerge: %v", mergeCtx.pr, err) // the merge proceeds unsigned regardless, so log it here + } if sign { mergeCtx.signKey = key if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { diff --git a/services/pull/merge_rebase.go b/services/pull/merge_rebase.go index 57b771e36c..0a2bca3940 100644 --- a/services/pull/merge_rebase.go +++ b/services/pull/merge_rebase.go @@ -59,7 +59,7 @@ func doMergeRebaseFastForward(ctx *mergeContext) error { } // Original repo to read template from. - baseGitRepo, err := gitrepo.OpenRepository(ctx, ctx.pr.BaseRepo) + baseGitRepo, err := gitrepo.OpenRepository(ctx.pr.BaseRepo) if err != nil { log.Error("Unable to get Git repo for rebase: %v", err) return err diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go index 54b91594fb..49bbe11b34 100644 --- a/services/pull/merge_squash.go +++ b/services/pull/merge_squash.go @@ -25,14 +25,14 @@ func getAuthorSignatureSquash(ctx *mergeContext) (*git.Signature, error) { // Try to get a signature from the same user in one of the commits, as the // poster email might be private or commits might have a different signature // than the primary email address of the poster. - gitRepo, err := git.OpenRepository(ctx, ctx.tmpBasePath) + gitRepo, err := git.OpenRepositoryLocal(ctx.tmpBasePath) if err != nil { log.Error("%-v Unable to open base repository: %v", ctx.pr, err) return nil, err } defer gitRepo.Close() - commits, err := gitRepo.CommitsBetween(git.RefNameFromBranch(tmpRepoTrackingBranch), git.RefNameHead, -1) + commits, err := gitRepo.CommitsBetween(ctx, git.RefNameFromBranch(tmpRepoTrackingBranch), git.RefNameHead, -1) if err != nil { log.Error("%-v Unable to get commits between: head and tracking branch: %v", ctx.pr, err) return nil, err diff --git a/services/pull/merge_tree.go b/services/pull/merge_tree.go index 10bdcf99f9..ded501a123 100644 --- a/services/pull/merge_tree.go +++ b/services/pull/merge_tree.go @@ -55,7 +55,7 @@ func checkPullRequestMergeableByMergeTree(ctx context.Context, pr *issues_model. if err := pr.LoadHeadRepo(ctx); err != nil { return err } - headGitRepo, err := gitrepo.OpenRepository(ctx, pr.HeadRepo) + headGitRepo, err := gitrepo.OpenRepository(pr.HeadRepo) if err != nil { return fmt.Errorf("OpenRepository: %w", err) } @@ -66,7 +66,7 @@ func checkPullRequestMergeableByMergeTree(ctx context.Context, pr *issues_model. if pr.IsSameRepo() { baseGitRepo = headGitRepo } else { - baseGitRepo, err = gitrepo.OpenRepository(ctx, pr.BaseRepo) + baseGitRepo, err = gitrepo.OpenRepository(pr.BaseRepo) if err != nil { return fmt.Errorf("OpenRepository: %w", err) } @@ -75,13 +75,13 @@ func checkPullRequestMergeableByMergeTree(ctx context.Context, pr *issues_model. // 3. Get head commit id if pr.Flow == issues_model.PullRequestFlowGithub { - pr.HeadCommitID, err = headGitRepo.GetRefCommitID(git.BranchPrefix + pr.HeadBranch) + pr.HeadCommitID, err = headGitRepo.GetRefCommitID(ctx, git.BranchPrefix+pr.HeadBranch) if err != nil { return fmt.Errorf("GetBranchCommitID: can't find commit ID for head: %w", err) } } else { if pr.ID > 0 { - pr.HeadCommitID, err = baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + pr.HeadCommitID, err = baseGitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) if err != nil { return fmt.Errorf("GetRefCommitID: can't find commit ID for head: %w", err) } @@ -93,7 +93,7 @@ func checkPullRequestMergeableByMergeTree(ctx context.Context, pr *issues_model. // 4. fetch head commit id into the current repository // it will be checked in 2 weeks by default from git if the pull request created failure. if !pr.IsSameRepo() { - if !baseGitRepo.IsReferenceExist(pr.HeadCommitID) { + if !baseGitRepo.IsReferenceExist(ctx, pr.HeadCommitID) { if err := gitrepo.FetchRemoteCommit(ctx, pr.BaseRepo, pr.HeadRepo, pr.HeadCommitID); err != nil { return fmt.Errorf("FetchRemoteCommit: %w", err) } @@ -101,7 +101,7 @@ func checkPullRequestMergeableByMergeTree(ctx context.Context, pr *issues_model. } // 5. update merge base - baseCommitID, err := baseGitRepo.GetRefCommitID(git.BranchPrefix + pr.BaseBranch) + baseCommitID, err := baseGitRepo.GetRefCommitID(ctx, git.BranchPrefix+pr.BaseBranch) if err != nil { return fmt.Errorf("GetBranchCommitID: can't find commit ID for base: %w", err) } diff --git a/services/pull/patch.go b/services/pull/patch.go index 3844072553..63d7a68eb5 100644 --- a/services/pull/patch.go +++ b/services/pull/patch.go @@ -39,11 +39,11 @@ func DownloadDiffOrPatch(ctx context.Context, pr *issues_model.PullRequest, w io compareArg := pr.MergeBase + "..." + pr.GetGitHeadRefName() switch { case patch: - err = gitRepo.GetPatch(compareArg, w) + err = gitRepo.GetPatch(ctx, compareArg, w) case binary: - err = gitRepo.GetDiffBinary(compareArg, w) + err = gitRepo.GetDiffBinary(ctx, compareArg, w) default: - err = gitRepo.GetDiff(compareArg, w) + err = gitRepo.GetDiff(ctx, compareArg, w) } if err != nil { @@ -74,7 +74,7 @@ func checkPullRequestMergeableByTmpRepo(ctx context.Context, pr *issues_model.Pu } defer cancel() - gitRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath) + gitRepo, err := git.OpenRepositoryLocal(prCtx.tmpBasePath) if err != nil { return fmt.Errorf("OpenRepository: %w", err) } @@ -84,13 +84,13 @@ func checkPullRequestMergeableByTmpRepo(ctx context.Context, pr *issues_model.Pu pr.MergeBase, _, err = gitcmd.NewCommand("merge-base", "--", tmpRepoBaseBranch, tmpRepoTrackingBranch).WithDir(prCtx.tmpBasePath).RunStdString(ctx) if err != nil { var err2 error - pr.MergeBase, err2 = gitRepo.GetRefCommitID(git.BranchPrefix + tmpRepoBaseBranch) + pr.MergeBase, err2 = gitRepo.GetRefCommitID(ctx, git.BranchPrefix+tmpRepoBaseBranch) if err2 != nil { return fmt.Errorf("GetMergeBase: %v and can't find commit ID for base: %w", err, err2) } } pr.MergeBase = strings.TrimSpace(pr.MergeBase) - if pr.HeadCommitID, err = gitRepo.GetRefCommitID(git.BranchPrefix + tmpRepoTrackingBranch); err != nil { + if pr.HeadCommitID, err = gitRepo.GetRefCommitID(ctx, git.BranchPrefix+tmpRepoTrackingBranch); err != nil { return fmt.Errorf("GetBranchCommitID: can't find commit ID for head: %w", err) } @@ -289,10 +289,10 @@ func AttemptThreeWayMerge(ctx context.Context, gitPath string, gitRepo *git.Repo } // Add and remove files in one command, as this is slow with many files otherwise - if err := gitRepo.RemoveFilesFromIndex(filesToRemove...); err != nil { + if err := gitRepo.RemoveFilesFromIndex(ctx, filesToRemove...); err != nil { return false, nil, err } - if err := gitRepo.AddObjectsToIndex(filesToAdd...); err != nil { + if err := gitRepo.AddObjectsToIndex(ctx, filesToAdd...); err != nil { return false, nil, err } @@ -321,7 +321,7 @@ func checkConflictsByTmpRepo(ctx context.Context, pr *issues_model.PullRequest, return false, fmt.Errorf("unable to write unconflicted tree: %w\n`git ls-files -u`:\n%s", err, lsfiles) } treeHash = strings.TrimSpace(treeHash) - baseTree, err := gitRepo.GetTree(tmpRepoBaseBranch) + baseTree, err := gitRepo.GetTree(ctx, tmpRepoBaseBranch) if err != nil { return false, err } @@ -366,11 +366,11 @@ func (err ErrFilePathProtected) Unwrap() error { } // CheckFileProtection check file Protection -func CheckFileProtection(repo *git.Repository, branchName, oldCommitID, newCommitID string, patterns []glob.Glob, limit int, env []string) ([]string, error) { +func CheckFileProtection(ctx context.Context, repo *git.Repository, branchName, oldCommitID, newCommitID string, patterns []glob.Glob, limit int, env []string) ([]string, error) { if len(patterns) == 0 { return nil, nil } - affectedFiles, err := git.GetAffectedFiles(repo, branchName, oldCommitID, newCommitID, env) + affectedFiles, err := git.GetAffectedFiles(ctx, repo, branchName, oldCommitID, newCommitID, env) if err != nil { return nil, err } @@ -396,11 +396,11 @@ func CheckFileProtection(repo *git.Repository, branchName, oldCommitID, newCommi } // CheckUnprotectedFiles check if the commit only touches unprotected files -func CheckUnprotectedFiles(repo *git.Repository, branchName, oldCommitID, newCommitID string, patterns []glob.Glob, env []string) (bool, error) { +func CheckUnprotectedFiles(ctx context.Context, repo *git.Repository, branchName, oldCommitID, newCommitID string, patterns []glob.Glob, env []string) (bool, error) { if len(patterns) == 0 { return false, nil } - affectedFiles, err := git.GetAffectedFiles(repo, branchName, oldCommitID, newCommitID, env) + affectedFiles, err := git.GetAffectedFiles(ctx, repo, branchName, oldCommitID, newCommitID, env) if err != nil { return false, err } @@ -437,7 +437,7 @@ func checkPullFilesProtection(ctx context.Context, pr *issues_model.PullRequest, return nil } - pr.ChangedProtectedFiles, err = CheckFileProtection(gitRepo, pr.HeadBranch, pr.MergeBase, headRef, pb.GetProtectedFilePatterns(), 10, os.Environ()) + pr.ChangedProtectedFiles, err = CheckFileProtection(ctx, gitRepo, pr.HeadBranch, pr.MergeBase, headRef, pb.GetProtectedFilePatterns(), 10, os.Environ()) if err != nil && !IsErrFilePathProtected(err) { return err } diff --git a/services/pull/pull.go b/services/pull/pull.go index 13f8ffa8df..8c08bba56a 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -326,6 +326,14 @@ func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer return fmt.Errorf("syncCommitDivergence: %w", err) } + // The "official" flag of existing reviews was computed against the previous + // target branch's protection rules, so re-evaluate it against the new branch. + // Otherwise a stale official approval could bypass the new branch's protection. + pr.Issue.PullRequest = pr + if err := issues_model.RecalculateReviewsOfficial(ctx, pr.Issue); err != nil { + return fmt.Errorf("RecalculateReviewsOfficial: %w", err) + } + // Create comment options := &issues_model.CreateCommentOptions{ Type: issues_model.CommentTypeChangeTargetBranch, @@ -357,7 +365,7 @@ func checkForInvalidation(ctx context.Context, requests issues_model.PullRequest if err != nil { return fmt.Errorf("GetRepositoryByIDCtx: %w", err) } - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + gitRepo, err := gitrepo.OpenRepository(repo) if err != nil { return fmt.Errorf("gitrepo.OpenRepository: %w", err) } @@ -769,30 +777,25 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re } // GetSquashMergeCommitMessages returns the commit messages between head and merge base (if there is one) -func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequest) string { +func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequest) (_ string, err error) { if err := pr.LoadIssue(ctx); err != nil { - log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err) - return "" + return "", err } if err := pr.Issue.LoadPoster(ctx); err != nil { - log.Error("Cannot load poster %d for pr id %d, index %d Error: %v", pr.Issue.PosterID, pr.ID, pr.Index, err) - return "" + return "", err } if pr.HeadRepo == nil { - var err error pr.HeadRepo, err = repo_model.GetRepositoryByID(ctx, pr.HeadRepoID) if err != nil { - log.Error("GetRepositoryByIdCtx[%d]: %v", pr.HeadRepoID, err) - return "" + return "", err } } gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.HeadRepo) if err != nil { - log.Error("Unable to open head repository: Error: %v", err) - return "" + return "", err } defer closer.Close() @@ -800,10 +803,9 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ if pr.Flow == issues_model.PullRequestFlowGithub { headCommitRef = git.RefNameFromBranch(pr.HeadBranch) } else { - pr.HeadCommitID, err = gitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + pr.HeadCommitID, err = gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) if err != nil { - log.Error("Unable to get head commit: %s Error: %v", pr.GetGitHeadRefName(), err) - return "" + return "", err } headCommitRef = git.RefNameFromCommit(pr.HeadCommitID) } @@ -812,10 +814,9 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ limit := setting.Repository.PullRequest.DefaultMergeMessageCommitsLimit - limitedCommits, err := gitRepo.CommitsBetween(headCommitRef, mergeBaseRef, limit) + limitedCommits, err := gitRepo.CommitsBetween(ctx, headCommitRef, mergeBaseRef, limit) if err != nil { - log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err) - return "" + return "", err } mergeMessage := strings.TrimSpace(pr.Issue.Content) // use PR's title and description as squash commit message @@ -823,7 +824,7 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ mergeMessage = formatSquashMergeCommitMessages(limitedCommits) // use PR's commit messages as squash commit message } coAuthors := collectSquashMergeCommitCoAuthors(ctx, gitRepo, pr, headCommitRef, mergeBaseRef, limit, limitedCommits) - return buildSquashMergeCommitMessages(mergeMessage, coAuthors) + return buildSquashMergeCommitMessages(mergeMessage, coAuthors), nil } func buildSquashMergeCommitMessages(mergeMessage string, coAuthors []string) string { @@ -873,7 +874,7 @@ func collectSquashMergeCommitCoAuthors(ctx context.Context, gitRepo *git.Reposit skip := limitFirst batchLimit := 30 for { - commits, err := gitRepo.CommitsBetween(headCommitRef, mergeBaseRef, batchLimit, skip) + commits, err := gitRepo.CommitsBetween(ctx, headCommitRef, mergeBaseRef, batchLimit, skip) if err != nil { log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err) return authors @@ -959,7 +960,7 @@ func GetIssuesAllCommitStatus(ctx context.Context, issues issues_model.IssueList } gitRepo, ok := gitRepos[issue.RepoID] if !ok { - gitRepo, err = gitrepo.OpenRepository(ctx, issue.Repo) + gitRepo, err = gitrepo.OpenRepository(issue.Repo) if err != nil { log.Error("Cannot open git repository %-v for issue #%d[%d]. Error: %v", issue.Repo, issue.Index, issue.ID, err) continue @@ -980,7 +981,7 @@ func GetIssuesAllCommitStatus(ctx context.Context, issues issues_model.IssueList // getAllCommitStatus get pr's commit statuses. func getAllCommitStatus(ctx context.Context, gitRepo *git.Repository, pr *issues_model.PullRequest) (statuses []*git_model.CommitStatus, lastStatus *git_model.CommitStatus, err error) { - sha, shaErr := gitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + sha, shaErr := gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) if shaErr != nil { return nil, nil, shaErr } @@ -1002,7 +1003,7 @@ func IsHeadEqualWithBranch(ctx context.Context, pr *issues_model.PullRequest, br } defer closer.Close() - baseCommit, err := baseGitRepo.GetBranchCommit(branchName) + baseCommit, err := baseGitRepo.GetBranchCommit(ctx, branchName) if err != nil { return false, err } @@ -1025,20 +1026,20 @@ func IsHeadEqualWithBranch(ctx context.Context, pr *issues_model.PullRequest, br var headCommit *git.Commit if pr.Flow == issues_model.PullRequestFlowGithub { - headCommit, err = headGitRepo.GetBranchCommit(pr.HeadBranch) + headCommit, err = headGitRepo.GetBranchCommit(ctx, pr.HeadBranch) if err != nil { return false, err } } else { - pr.HeadCommitID, err = baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + pr.HeadCommitID, err = baseGitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) if err != nil { return false, err } - if headCommit, err = baseGitRepo.GetCommit(pr.HeadCommitID); err != nil { + if headCommit, err = baseGitRepo.GetCommit(ctx, pr.HeadCommitID); err != nil { return false, err } } - return baseCommit.HasPreviousCommit(headCommit.ID) + return baseCommit.HasPreviousCommit(ctx, baseGitRepo, headCommit.ID) } type CommitInfo struct { diff --git a/services/pull/pull_test.go b/services/pull/pull_test.go index 554c9ed577..674e4f642d 100644 --- a/services/pull/pull_test.go +++ b/services/pull/pull_test.go @@ -45,7 +45,7 @@ func TestPullRequest_GetDefaultMergeMessage_InternalTracker(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) assert.NoError(t, pr.LoadBaseRepo(t.Context())) - gitRepo, err := gitrepo.OpenRepository(t.Context(), pr.BaseRepo) + gitRepo, err := gitrepo.OpenRepository(pr.BaseRepo) assert.NoError(t, err) defer gitRepo.Close() @@ -75,7 +75,7 @@ func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2, BaseRepo: baseRepo}) assert.NoError(t, pr.LoadBaseRepo(t.Context())) - gitRepo, err := gitrepo.OpenRepository(t.Context(), pr.BaseRepo) + gitRepo, err := gitrepo.OpenRepository(pr.BaseRepo) assert.NoError(t, err) defer gitRepo.Close() diff --git a/services/pull/review.go b/services/pull/review.go index 18abdb04cd..a3ff735e4f 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -66,7 +66,7 @@ func lineBlame(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Re } objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) - return gitRepo.GetCommit(sha[:objectFormat.FullLength()]) + return gitRepo.GetCommit(ctx, sha[:objectFormat.FullLength()]) } // checkInvalidation checks if the line of code comment got changed by another commit. @@ -266,7 +266,7 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo // Only fetch diff if comment is review comment if len(patch) == 0 && reviewID != 0 { - headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + headCommitID, err := gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) if err != nil { return nil, fmt.Errorf("GetRefCommitID[%s]: %w", pr.GetGitHeadRefName(), err) } @@ -274,7 +274,7 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo commitID = headCommitID } - patch, err = git.GetFileDiffCutAroundLine( + patch, err = git.GetFileDiffCutAroundLine(ctx, gitRepo, pr.MergeBase, headCommitID, treePath, int64((&issues_model.Comment{Line: line}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines, ) @@ -284,7 +284,7 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo // If patch is still empty (unchanged line), generate code context if patch == "" && commitID != "" { - patch, err = gitdiff.GeneratePatchForUnchangedLine(gitRepo, commitID, treePath, line, setting.UI.CodeCommentLines) + patch, err = gitdiff.GeneratePatchForUnchangedLine(ctx, gitRepo, commitID, treePath, line, setting.UI.CodeCommentLines) if err != nil { // Log the error but don't fail comment creation log.Debug("Unable to generate patch for unchanged line (file=%s, line=%d, commit=%s): %v", treePath, line, commitID, err) @@ -322,7 +322,7 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos return nil, nil, ErrSubmitReviewOnClosedPR } - headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + headCommitID, err := gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) if err != nil { return nil, nil, err } diff --git a/services/release/notes.go b/services/release/notes.go index bf41d3426f..2dd7c9f731 100644 --- a/services/release/notes.go +++ b/services/release/notes.go @@ -28,7 +28,7 @@ type GenerateReleaseNotesOptions struct { // GenerateReleaseNotes builds the Markdown snippet for release notes. func GenerateReleaseNotes(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts GenerateReleaseNotesOptions) (string, error) { - headCommit, err := resolveHeadCommit(gitRepo, opts.TagName, opts.TagTarget) + headCommit, err := resolveHeadCommit(ctx, gitRepo, opts.TagName, opts.TagTarget) if err != nil { return "", err } @@ -40,7 +40,7 @@ func GenerateReleaseNotes(ctx context.Context, repo *repo_model.Repository, gitR var baseCommitID git.RefName if opts.PreviousTag != "" { - baseCommit, err := gitRepo.GetCommit(opts.PreviousTag) + baseCommit, err := gitRepo.GetCommit(ctx, opts.PreviousTag) if err != nil { return "", util.ErrorWrapTranslatable(util.ErrNotExist, "repo.release.generate_notes_tag_not_found", opts.PreviousTag) } @@ -49,7 +49,7 @@ func GenerateReleaseNotes(ctx context.Context, repo *repo_model.Repository, gitR return "", util.ErrorWrapTranslatable(util.ErrNotExist, "repo.release.generate_notes_tag_not_found", opts.TagName) } - commits, err := gitRepo.CommitsBetween(headCommit.ID.RefName(), baseCommitID, -1) + commits, err := gitRepo.CommitsBetween(ctx, headCommit.ID.RefName(), baseCommitID, -1) if err != nil { return "", fmt.Errorf("CommitsBetween: %w", err) } @@ -85,13 +85,13 @@ func repoReleaseIsEmpty(ctx context.Context, repoID int64) (bool, error) { return count == 0, nil } -func resolveHeadCommit(gitRepo *git.Repository, tagName, tagTarget string) (*git.Commit, error) { +func resolveHeadCommit(ctx context.Context, gitRepo *git.Repository, tagName, tagTarget string) (*git.Commit, error) { ref := tagName - if !gitRepo.IsTagExist(tagName) { + if !gitRepo.IsTagExist(ctx, tagName) { ref = tagTarget } - commit, err := gitRepo.GetCommit(ref) + commit, err := gitRepo.GetCommit(ctx, ref) if err != nil { return nil, util.ErrorWrapTranslatable(util.ErrNotExist, "repo.release.generate_notes_target_not_found", ref) } diff --git a/services/release/notes_test.go b/services/release/notes_test.go index 3fb3b7c553..df050d9243 100644 --- a/services/release/notes_test.go +++ b/services/release/notes_test.go @@ -23,7 +23,7 @@ func TestGenerateReleaseNotes(t *testing.T) { t.Run("ChangeLogsWithPRs", func(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - gitRepo, err := gitrepo.OpenRepository(t.Context(), repo) + gitRepo, err := gitrepo.OpenRepository(repo) require.NoError(t, err) t.Cleanup(func() { gitRepo.Close() }) @@ -52,7 +52,7 @@ func TestGenerateReleaseNotes(t *testing.T) { t.Run("NoPreviousTag", func(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) - gitRepo, err := gitrepo.OpenRepository(t.Context(), repo) + gitRepo, err := gitrepo.OpenRepository(repo) require.NoError(t, err) t.Cleanup(func() { gitRepo.Close() }) @@ -83,7 +83,7 @@ func TestGenerateReleaseNotes(t *testing.T) { t.Run("EmptyPreviousTagWithExistingTags", func(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - gitRepo, err := gitrepo.OpenRepository(t.Context(), repo) + gitRepo, err := gitrepo.OpenRepository(repo) require.NoError(t, err) t.Cleanup(func() { gitRepo.Close() }) diff --git a/services/release/release.go b/services/release/release.go index 1be60731a9..66e8957a1a 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -20,9 +20,11 @@ import ( "gitea.dev/modules/graceful" "gitea.dev/modules/log" "gitea.dev/modules/repository" + "gitea.dev/modules/setting" "gitea.dev/modules/storage" "gitea.dev/modules/timeutil" "gitea.dev/modules/util" + "gitea.dev/services/context/upload" notify_service "gitea.dev/services/notify" ) @@ -101,13 +103,13 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel } } - commit, err := gitRepo.GetCommit(rel.Target) + commit, err := gitRepo.GetCommit(ctx, rel.Target) if err != nil { return false, err } if len(msg) > 0 { - if err = gitRepo.CreateAnnotatedTag(rel.TagName, msg, commit.ID.String()); err != nil { + if err = gitRepo.CreateAnnotatedTag(ctx, rel.TagName, msg, commit.ID.String()); err != nil { if strings.Contains(err.Error(), "is not a valid tag name") { return false, ErrInvalidTagName{ TagName: rel.TagName, @@ -115,7 +117,7 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel } return false, err } - } else if err = gitRepo.CreateTag(rel.TagName, commit.ID.String()); err != nil { + } else if err = gitRepo.CreateTag(ctx, rel.TagName, commit.ID.String()); err != nil { if strings.Contains(err.Error(), "is not a valid tag name") { return false, ErrInvalidTagName{ TagName: rel.TagName, @@ -142,7 +144,7 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel notify_service.CreateRef(ctx, rel.Publisher, rel.Repo, refFullName, commit.ID.String()) rel.CreatedUnix = timeutil.TimeStampNow() } - commit, err := gitRepo.GetTagCommit(rel.TagName) + commit, err := gitRepo.GetTagCommit(ctx, rel.TagName) if err != nil { return false, fmt.Errorf("GetTagCommit: %w", err) } @@ -166,8 +168,8 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel } // CreateRelease creates a new release of repository. -func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentUUIDs []string, msg string) error { - has, err := repo_model.IsReleaseExist(gitRepo.Ctx, rel.RepoID, rel.TagName) +func CreateRelease(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Release, attachmentUUIDs []string, msg string) error { + has, err := repo_model.IsReleaseExist(ctx, rel.RepoID, rel.TagName) if err != nil { return err } else if has { @@ -176,22 +178,22 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentU } } - if _, err = createTag(gitRepo.Ctx, gitRepo, rel, msg); err != nil { + if _, err = createTag(ctx, gitRepo, rel, msg); err != nil { return err } rel.Title = util.EllipsisDisplayString(rel.Title, 255) rel.LowerTagName = strings.ToLower(rel.TagName) - if err = db.Insert(gitRepo.Ctx, rel); err != nil { + if err = db.Insert(ctx, rel); err != nil { return err } - if err = repo_model.AddReleaseAttachments(gitRepo.Ctx, rel.ID, attachmentUUIDs); err != nil { + if err = repo_model.AddReleaseAttachments(ctx, rel.ID, attachmentUUIDs); err != nil { return err } if !rel.IsDraft { - notify_service.NewRelease(gitRepo.Ctx, rel) + notify_service.NewRelease(ctx, rel) } return nil @@ -319,13 +321,17 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo } for uuid, newName := range editAttachments { - if !deletedUUIDs.Contains(uuid) { - if err = repo_model.UpdateAttachmentByUUID(ctx, &repo_model.Attachment{ - UUID: uuid, - Name: newName, - }, "name"); err != nil { - return err - } + if deletedUUIDs.Contains(uuid) { + continue + } + if err = upload.Verify(nil, newName, setting.Repository.Release.AllowedTypes); err != nil { + return err + } + if err = repo_model.UpdateAttachmentByUUID(ctx, &repo_model.Attachment{ + UUID: uuid, + Name: newName, + }, "name"); err != nil { + return err } } } @@ -346,10 +352,10 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo if !rel.IsDraft { if !isTagCreated && !isConvertedFromTag { - notify_service.UpdateRelease(gitRepo.Ctx, doer, rel) + notify_service.UpdateRelease(ctx, doer, rel) return nil } - notify_service.NewRelease(gitRepo.Ctx, rel) + notify_service.NewRelease(ctx, rel) } return nil } diff --git a/services/release/release_test.go b/services/release/release_test.go index 579d92f2f4..b9f029da9b 100644 --- a/services/release/release_test.go +++ b/services/release/release_test.go @@ -12,8 +12,11 @@ import ( "gitea.dev/models/unittest" user_model "gitea.dev/models/user" "gitea.dev/modules/gitrepo" + "gitea.dev/modules/setting" + "gitea.dev/modules/test" "gitea.dev/modules/timeutil" "gitea.dev/services/attachment" + "gitea.dev/services/context/upload" _ "gitea.dev/models/actions" @@ -30,11 +33,11 @@ func TestRelease_Create(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - gitRepo, err := gitrepo.OpenRepository(t.Context(), repo) + gitRepo, err := gitrepo.OpenRepository(repo) assert.NoError(t, err) defer gitRepo.Close() - assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{ + assert.NoError(t, CreateRelease(t.Context(), gitRepo, &repo_model.Release{ RepoID: repo.ID, Repo: repo, PublisherID: user.ID, @@ -48,7 +51,7 @@ func TestRelease_Create(t *testing.T) { IsTag: false, }, nil, "")) - assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{ + assert.NoError(t, CreateRelease(t.Context(), gitRepo, &repo_model.Release{ RepoID: repo.ID, Repo: repo, PublisherID: user.ID, @@ -62,7 +65,7 @@ func TestRelease_Create(t *testing.T) { IsTag: false, }, nil, "")) - assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{ + assert.NoError(t, CreateRelease(t.Context(), gitRepo, &repo_model.Release{ RepoID: repo.ID, Repo: repo, PublisherID: user.ID, @@ -76,7 +79,7 @@ func TestRelease_Create(t *testing.T) { IsTag: false, }, nil, "")) - assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{ + assert.NoError(t, CreateRelease(t.Context(), gitRepo, &repo_model.Release{ RepoID: repo.ID, Repo: repo, PublisherID: user.ID, @@ -90,7 +93,7 @@ func TestRelease_Create(t *testing.T) { IsTag: false, }, nil, "")) - assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{ + assert.NoError(t, CreateRelease(t.Context(), gitRepo, &repo_model.Release{ RepoID: repo.ID, Repo: repo, PublisherID: user.ID, @@ -126,7 +129,7 @@ func TestRelease_Create(t *testing.T) { IsPrerelease: false, IsTag: true, } - assert.NoError(t, CreateRelease(gitRepo, &release, []string{attach.UUID}, "test")) + assert.NoError(t, CreateRelease(t.Context(), gitRepo, &release, []string{attach.UUID}, "test")) } func TestRelease_Update(t *testing.T) { @@ -135,7 +138,7 @@ func TestRelease_Update(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - gitRepo, err := gitrepo.OpenRepository(t.Context(), repo) + gitRepo, err := gitrepo.OpenRepository(repo) assert.NoError(t, err) defer gitRepo.Close() @@ -146,7 +149,7 @@ func TestRelease_Update(t *testing.T) { advance := func() { fakeNow = fakeNow.Add(time.Second); timeutil.MockSet(fakeNow) } // Test a changed release - assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{ + assert.NoError(t, CreateRelease(t.Context(), gitRepo, &repo_model.Release{ RepoID: repo.ID, Repo: repo, PublisherID: user.ID, @@ -170,7 +173,7 @@ func TestRelease_Update(t *testing.T) { assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix)) // Test a changed draft - assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{ + assert.NoError(t, CreateRelease(t.Context(), gitRepo, &repo_model.Release{ RepoID: repo.ID, Repo: repo, PublisherID: user.ID, @@ -194,7 +197,7 @@ func TestRelease_Update(t *testing.T) { assert.Less(t, int64(releaseCreatedUnix), int64(release.CreatedUnix)) // Test a changed pre-release - assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{ + assert.NoError(t, CreateRelease(t.Context(), gitRepo, &repo_model.Release{ RepoID: repo.ID, Repo: repo, PublisherID: user.ID, @@ -232,7 +235,7 @@ func TestRelease_Update(t *testing.T) { IsPrerelease: false, IsTag: false, } - assert.NoError(t, CreateRelease(gitRepo, release, nil, "")) + assert.NoError(t, CreateRelease(t.Context(), gitRepo, release, nil, "")) assert.Positive(t, release.ID) release.IsDraft = false @@ -270,6 +273,17 @@ func TestRelease_Update(t *testing.T) { assert.Equal(t, release.ID, release.Attachments[0].ReleaseID) assert.Equal(t, "test2.txt", release.Attachments[0].Name) + defer test.MockVariableValue(&setting.Repository.Release.AllowedTypes, ".zip")() + err = UpdateRelease(t.Context(), user, gitRepo, release, nil, nil, map[string]string{ + attach.UUID: "test.exe", + }) + assert.Error(t, err) + assert.True(t, upload.IsErrFileTypeForbidden(err)) + release.Attachments = nil + assert.NoError(t, repo_model.GetReleaseAttachments(t.Context(), release)) + assert.Len(t, release.Attachments, 1) + assert.Equal(t, "test2.txt", release.Attachments[0].Name) + // delete the attachment assert.NoError(t, UpdateRelease(t.Context(), user, gitRepo, release, nil, []string{attach.UUID}, nil)) release.Attachments = nil @@ -283,7 +297,7 @@ func TestRelease_createTag(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - gitRepo, err := gitrepo.OpenRepository(t.Context(), repo) + gitRepo, err := gitrepo.OpenRepository(repo) assert.NoError(t, err) defer gitRepo.Close() diff --git a/services/repository/adopt.go b/services/repository/adopt.go index a05cfb66df..e08f32499d 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -141,7 +141,7 @@ func adoptRepository(ctx context.Context, repo *repo_model.Repository, defaultBr } // Don't bother looking this repo in the context it won't be there - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + gitRepo, err := gitrepo.OpenRepository(repo) if err != nil { return fmt.Errorf("openRepository: %w", err) } @@ -213,10 +213,10 @@ func DeleteUnadoptedRepository(ctx context.Context, doer, u *user_model.User, re return err } - relativePath := repo_model.RelativePath(u.Name, repoName) - exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(relativePath)) + codeRepo := repo_model.CodeRepoByName(u.Name, repoName) + exist, err := gitrepo.IsRepositoryExist(ctx, codeRepo) if err != nil { - log.Error("Unable to check if %s exists. Error: %v", relativePath, err) + log.Error("Unable to check if repo %s/%s exists. Error: %v", u.Name, repoName, err) return err } if !exist { @@ -235,7 +235,7 @@ func DeleteUnadoptedRepository(ctx context.Context, doer, u *user_model.User, re } } - return gitrepo.DeleteRepository(ctx, repo_model.StorageRepo(relativePath)) + return gitrepo.DeleteRepository(ctx, codeRepo) } type unadoptedRepositories struct { diff --git a/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go index 2b78118556..e7c0d12616 100644 --- a/services/repository/archiver/archiver.go +++ b/services/repository/archiver/archiver.go @@ -42,10 +42,42 @@ type ArchiveRequest struct { archiveRefShortName string // the ref short name to download the archive, for example: "master", "v1.0.0", "commit id" } +type archiveQueueItem struct { + RepoID int64 `json:"RepoID"` + Type repo_model.ArchiveType `json:"Type"` + CommitID string `json:"CommitID"` + Paths []string `json:"Paths,omitempty"` + ArchiveRefShortName string `json:"ArchiveRefShortName,omitempty"` +} + +func (aReq *ArchiveRequest) toQueueItem() *archiveQueueItem { + return &archiveQueueItem{ + RepoID: aReq.Repo.ID, + Type: aReq.Type, + CommitID: aReq.CommitID, + Paths: aReq.Paths, + ArchiveRefShortName: aReq.archiveRefShortName, + } +} + +func (item *archiveQueueItem) toArchiveRequest(ctx context.Context) (*ArchiveRequest, error) { + repo, err := repo_model.GetRepositoryByID(ctx, item.RepoID) + if err != nil { + return nil, err + } + return &ArchiveRequest{ + Repo: repo, + Type: item.Type, + CommitID: item.CommitID, + Paths: item.Paths, + archiveRefShortName: item.ArchiveRefShortName, + }, nil +} + // NewRequest creates an archival request, based on the URI. The // resulting ArchiveRequest is suitable for being passed to Await() // if it's determined that the request still needs to be satisfied. -func NewRequest(repo *repo_model.Repository, gitRepo *git.Repository, archiveRefExt string, paths []string) (*ArchiveRequest, error) { +func NewRequest(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, archiveRefExt string, paths []string) (*ArchiveRequest, error) { // here the archiveRefShortName is not a clear ref, it could be a tag, branch or commit id archiveRefShortName, archiveType := repo_model.SplitArchiveNameType(archiveRefExt) if archiveType == repo_model.ArchiveUnknown { @@ -56,7 +88,7 @@ func NewRequest(repo *repo_model.Repository, gitRepo *git.Repository, archiveRef } // Get corresponding commit. - commit, err := gitRepo.GetCommit(archiveRefShortName) + commit, err := gitRepo.GetCommit(ctx, archiveRefShortName) if err != nil { return nil, util.NewNotExistErrorf("unrecognized repository reference: %s", archiveRefShortName) } @@ -129,9 +161,9 @@ func (aReq *ArchiveRequest) Stream(ctx context.Context, w io.Writer) error { return gitrepo.CreateArchive( ctx, aReq.Repo, + aReq.Repo.Name, aReq.Type.String(), w, - setting.Repository.PrefixArchiveFiles, aReq.CommitID, aReq.Paths, ) @@ -227,13 +259,18 @@ func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver return archiver, nil } -var archiverQueue *queue.WorkerPoolQueue[*ArchiveRequest] +var archiverQueue *queue.WorkerPoolQueue[*archiveQueueItem] // Init initializes archiver func Init(ctx context.Context) error { - handler := func(items ...*ArchiveRequest) []*ArchiveRequest { - for _, archiveReq := range items { - log.Trace("ArchiverData Process: %#v", archiveReq) + handler := func(items ...*archiveQueueItem) []*archiveQueueItem { + for _, item := range items { + log.Trace("ArchiverData Process: %#v", item) + archiveReq, err := item.toArchiveRequest(ctx) + if err != nil { + log.Error("Archive repo %d: %v", item.RepoID, err) + continue + } if archiver, err := doArchive(ctx, archiveReq); err != nil { log.Error("Archive %v failed: %v", archiveReq, err) } else { @@ -254,14 +291,15 @@ func Init(ctx context.Context) error { // StartArchive push the archive request to the queue func StartArchive(request *ArchiveRequest) error { - has, err := archiverQueue.Has(request) + item := request.toQueueItem() + has, err := archiverQueue.Has(item) if err != nil { return err } if has { return nil } - return archiverQueue.Push(request) + return archiverQueue.Push(item) } func deleteOldRepoArchiver(ctx context.Context, archiver *repo_model.RepoArchiver) error { diff --git a/services/repository/archiver/archiver_test.go b/services/repository/archiver/archiver_test.go index 16a4750a8b..e9b2f52545 100644 --- a/services/repository/archiver/archiver_test.go +++ b/services/repository/archiver/archiver_test.go @@ -7,7 +7,9 @@ import ( "testing" "time" + repo_model "gitea.dev/models/repo" "gitea.dev/models/unittest" + "gitea.dev/modules/json" "gitea.dev/modules/util" "gitea.dev/services/contexttest" @@ -21,6 +23,22 @@ func TestMain(m *testing.M) { unittest.MainTest(m) } +func TestArchiveQueueItemJSON(t *testing.T) { + orig := &archiveQueueItem{ + RepoID: 7, + Type: repo_model.ArchiveZip, + CommitID: "abc123", + Paths: []string{"agents"}, + ArchiveRefShortName: "main", + } + bs, err := json.Marshal(orig) + require.NoError(t, err) + + var decoded archiveQueueItem + require.NoError(t, json.Unmarshal(bs, &decoded)) + assert.Equal(t, *orig, decoded) +} + func TestArchive_Basic(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) @@ -31,47 +49,47 @@ func TestArchive_Basic(t *testing.T) { contexttest.LoadGitRepo(t, ctx) defer ctx.Repo.GitRepo.Close() - bogusReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip", nil) + bogusReq, err := NewRequest(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip", nil) assert.NoError(t, err) assert.NotNil(t, bogusReq) assert.Equal(t, firstCommit+".zip", bogusReq.GetArchiveName()) // Check a series of bogus requests. // Step 1, valid commit with a bad extension. - bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".unknown", nil) + bogusReq, err = NewRequest(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".unknown", nil) assert.Error(t, err) assert.Nil(t, bogusReq) // Step 2, missing commit. - bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, "dbffff.zip", nil) + bogusReq, err = NewRequest(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, "dbffff.zip", nil) assert.Error(t, err) assert.Nil(t, bogusReq) // Step 3, doesn't look like branch/tag/commit. - bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, "db.zip", nil) + bogusReq, err = NewRequest(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, "db.zip", nil) assert.Error(t, err) assert.Nil(t, bogusReq) - bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, "master.zip", nil) + bogusReq, err = NewRequest(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, "master.zip", nil) assert.NoError(t, err) assert.NotNil(t, bogusReq) assert.Equal(t, "master.zip", bogusReq.GetArchiveName()) - bogusReq, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, "test/archive.zip", nil) + bogusReq, err = NewRequest(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, "test/archive.zip", nil) assert.NoError(t, err) assert.NotNil(t, bogusReq) assert.Equal(t, "test-archive.zip", bogusReq.GetArchiveName()) // Now two valid requests, firstCommit with valid extensions. - zipReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip", nil) + zipReq, err := NewRequest(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip", nil) assert.NoError(t, err) assert.NotNil(t, zipReq) - tgzReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".tar.gz", nil) + tgzReq, err := NewRequest(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".tar.gz", nil) assert.NoError(t, err) assert.NotNil(t, tgzReq) - secondReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, secondCommit+".bundle", nil) + secondReq, err := NewRequest(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, secondCommit+".bundle", nil) assert.NoError(t, err) assert.NotNil(t, secondReq) @@ -91,7 +109,7 @@ func TestArchive_Basic(t *testing.T) { // Sleep two seconds to make sure the queue doesn't change. time.Sleep(2 * time.Second) - zipReq2, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip", nil) + zipReq2, err := NewRequest(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip", nil) assert.NoError(t, err) // This zipReq should match what's sitting in the queue, as we haven't // let it release yet. From the consumer's point of view, this looks like @@ -106,12 +124,12 @@ func TestArchive_Basic(t *testing.T) { // Now we'll submit a request and TimedWaitForCompletion twice, before and // after we release it. We should trigger both the timeout and non-timeout // cases. - timedReq, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, secondCommit+".tar.gz", nil) + timedReq, err := NewRequest(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, secondCommit+".tar.gz", nil) assert.NoError(t, err) assert.NotNil(t, timedReq) doArchive(t.Context(), timedReq) - zipReq2, err = NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip", nil) + zipReq2, err = NewRequest(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".zip", nil) assert.NoError(t, err) // Now, we're guaranteed to have released the original zipReq from the queue. // Ensure that we don't get handed back the released entry somehow, but they @@ -128,7 +146,7 @@ func TestArchive_Basic(t *testing.T) { assert.NotEqual(t, zipReq.GetArchiveName(), secondReq.GetArchiveName()) t.Run("BadPath", func(t *testing.T) { - badRequest, err := NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".tar.gz", []string{"not-a-path"}) + badRequest, err := NewRequest(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, firstCommit+".tar.gz", []string{"not-a-path"}) require.NoError(t, err) err = ServeRepoArchive(ctx.Base, badRequest) require.Error(t, err) diff --git a/services/repository/branch.go b/services/repository/branch.go index 0869750db4..8d2391eb8d 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -38,13 +38,13 @@ import ( ) // CreateNewBranch creates a new repository branch -func CreateNewBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldBranchName, branchName string) (err error) { +func CreateNewBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, oldBranchName, branchName string) (err error) { branch, err := git_model.GetBranch(ctx, repo.ID, oldBranchName) if err != nil { return err } - return CreateNewBranchFromCommit(ctx, doer, repo, branch.CommitID, branchName) + return CreateNewBranchFromCommit(ctx, doer, repo, gitRepo, branch.CommitID, branchName) } // Branch contains the branch information @@ -226,14 +226,14 @@ func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *g if pr.HasMerged { baseGitRepo, ok := repoIDToGitRepo[pr.BaseRepoID] if !ok { - baseGitRepo, err = gitrepo.OpenRepository(ctx, pr.BaseRepo) + baseGitRepo, err = gitrepo.OpenRepository(pr.BaseRepo) if err != nil { return nil, fmt.Errorf("OpenRepository: %v", err) } defer baseGitRepo.Close() repoIDToGitRepo[pr.BaseRepoID] = baseGitRepo } - pullCommit, err := baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName()) + pullCommit, err := baseGitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) if err != nil && !git.IsErrNotExist(err) { return nil, fmt.Errorf("GetBranchCommitID: %v", err) } @@ -257,8 +257,8 @@ func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *g } // checkBranchName validates branch name with existing repository branches -func checkBranchName(ctx context.Context, repo *repo_model.Repository, name string) error { - _, err := gitrepo.WalkReferences(ctx, repo, func(_, refName string) error { +func checkBranchName(ctx context.Context, gitRepo *git.Repository, name string) error { + _, err := gitRepo.WalkReferences(ctx, "", 0, 0, func(_, refName string) error { branchRefName := strings.TrimPrefix(refName, git.BranchPrefix) switch { case branchRefName == name: @@ -289,7 +289,7 @@ func checkBranchName(ctx context.Context, repo *repo_model.Repository, name stri // It will check whether the branches of the repository have never been synced before. // If so, it will sync all branches of the repository. // Otherwise, it will sync the branches that need to be updated. -func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames, commitIDs []string, getCommit func(commitID string) (*git.Commit, error)) error { +func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, gitRepo *git.Repository, branchNames, commitIDs []string) error { // Some designs that make the code look strange but are made for performance optimization purposes: // 1. Sync branches in a batch to reduce the number of DB queries. // 2. Lazy load commit information since it may be not necessary. @@ -343,7 +343,7 @@ func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames, continue } - commit, err := getCommit(commitID) + commit, err := gitRepo.GetCommit(ctx, commitID) if err != nil { return fmt.Errorf("get commit of %s failed: %v", branchName, err) } @@ -374,14 +374,14 @@ func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames, } // CreateNewBranchFromCommit creates a new repository branch -func CreateNewBranchFromCommit(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commitID, branchName string) (err error) { +func CreateNewBranchFromCommit(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, commitID, branchName string) (err error) { err = repo.MustNotBeArchived() if err != nil { return err } // Check if branch name can be used - if err := checkBranchName(ctx, repo, branchName); err != nil { + if err := checkBranchName(ctx, gitRepo, branchName); err != nil { return err } @@ -504,7 +504,7 @@ func UpdateBranch(ctx context.Context, repo *repo_model.Repository, gitRepo *git } if expectedOldCommitID != "" { - expectedID, err := gitRepo.ConvertToGitID(expectedOldCommitID) + expectedID, err := gitRepo.ConvertToGitID(ctx, expectedOldCommitID) if err != nil { return fmt.Errorf("ConvertToGitID(old): %w", err) } @@ -513,11 +513,11 @@ func UpdateBranch(ctx context.Context, repo *repo_model.Repository, gitRepo *git } } - newID, err := gitRepo.ConvertToGitID(newCommitID) + newID, err := gitRepo.ConvertToGitID(ctx, newCommitID) if err != nil { return fmt.Errorf("ConvertToGitID(new): %w", err) } - newCommit, err := gitRepo.GetCommit(newID.String()) + newCommit, err := gitRepo.GetCommit(ctx, newID.String()) if err != nil { return err } @@ -526,7 +526,7 @@ func UpdateBranch(ctx context.Context, repo *repo_model.Repository, gitRepo *git return nil } - isForcePush, err := newCommit.IsForcePush(branch.CommitID) + isForcePush, err := newCommit.IsForcePush(ctx, gitRepo, branch.CommitID) if err != nil { return err } @@ -615,7 +615,7 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R return err } - branchCommit, err := gitRepo.GetBranchCommit(branchName) + branchCommit, err := gitRepo.GetBranchCommit(ctx, branchName) // branchCommit can be nil if the branch doesn't exist in git if err != nil && !errors.Is(err, util.ErrNotExist) { return err @@ -803,7 +803,7 @@ func GetBranchDivergingInfo(ctx reqctx.RequestContext, baseRepo *repo_model.Repo if err != nil { return nil, err } - headCommit, err := headGitRepo.GetCommit(headGitBranch.CommitID) + headCommit, err := headGitRepo.GetCommit(ctx, headGitBranch.CommitID) if err != nil { return nil, err } @@ -811,7 +811,7 @@ func GetBranchDivergingInfo(ctx reqctx.RequestContext, baseRepo *repo_model.Repo if err != nil { return nil, err } - hasPreviousCommit, _ := headCommit.HasPreviousCommit(baseCommitID) + hasPreviousCommit, _ := headCommit.HasPreviousCommit(ctx, headGitRepo, baseCommitID) info.BaseHasNewCommits = !hasPreviousCommit return info, nil } @@ -886,12 +886,12 @@ func DeleteBranchAfterMerge(ctx context.Context, doer *user_model.User, prID int defer gitHeadCloser.Close() // Check if branch has no new commits - headCommitID, err := gitBaseRepo.GetRefCommitID(pr.GetGitHeadRefName()) + headCommitID, err := gitBaseRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) if err != nil { log.Error("GetRefCommitID: %v", err) return errFailedToDelete(err) } - branchCommitID, err := gitHeadRepo.GetBranchCommitID(pr.HeadBranch) + branchCommitID, err := gitHeadRepo.GetBranchCommitID(ctx, pr.HeadBranch) if err != nil { log.Error("GetBranchCommitID: %v", err) return errFailedToDelete(err) diff --git a/services/repository/cache.go b/services/repository/cache.go index ff1955f182..1f08f042bc 100644 --- a/services/repository/cache.go +++ b/services/repository/cache.go @@ -14,7 +14,7 @@ import ( // CacheRef cachhe last commit information of the branch or the tag func CacheRef(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, fullRefName git.RefName) error { - commit, err := gitRepo.GetCommit(fullRefName.String()) + commit, err := gitRepo.GetCommit(ctx, fullRefName.String()) if err != nil { return err } @@ -29,5 +29,5 @@ func CacheRef(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Rep gitRepo.LastCommitCache = git.NewLastCommitCache(commitsCount, repo.FullName(), gitRepo, cache.GetCache()) } - return commit.CacheCommit(ctx) + return commit.CacheCommit(ctx, gitRepo) } diff --git a/services/repository/check.go b/services/repository/check.go index 8f73186181..8b921b0eb4 100644 --- a/services/repository/check.go +++ b/services/repository/check.go @@ -95,7 +95,7 @@ func GitGcRepo(ctx context.Context, repo *repo_model.Repository, timeout time.Du if err := system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } - return fmt.Errorf("Repository garbage collection failed in repo: %s: Error: %w", repo.RelativePath(), err) + return fmt.Errorf("repository garbage collection failed in repo: %s, error: %w", repo.FullName(), err) } // Now update the size of the repository @@ -105,7 +105,7 @@ func GitGcRepo(ctx context.Context, repo *repo_model.Repository, timeout time.Du if err := system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } - return fmt.Errorf("Updating size as part of garbage collection failed in repo: %s: Error: %w", repo.RelativePath(), err) + return fmt.Errorf("updating size as part of garbage collection failed in repo: %s, error: %w", repo.FullName(), err) } return nil @@ -190,7 +190,7 @@ func ReinitMissingRepositories(ctx context.Context) error { } log.Trace("Initializing %d/%d...", repo.OwnerID, repo.ID) if err := gitrepo.InitRepository(ctx, repo, repo.ObjectFormatName); err != nil { - log.Error("Unable (re)initialize repository %d at %s. Error: %v", repo.ID, repo.RelativePath(), err) + log.Error("Unable (re)initialize repository %d at %s. Error: %v", repo.ID, repo.FullName(), err) if err2 := system_model.CreateRepositoryNotice("InitRepository (%s) [%d]: %v", repo.FullName(), repo.ID, err); err2 != nil { log.Error("CreateRepositoryNotice: %v", err2) } diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go index 63b31a1c4b..9ff7f849d8 100644 --- a/services/repository/commitstatus/commitstatus.go +++ b/services/repository/commitstatus/commitstatus.go @@ -72,13 +72,13 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato // confirm that commit is exist gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) if err != nil { - return fmt.Errorf("OpenRepository[%s]: %w", repo.RelativePath(), err) + return fmt.Errorf("OpenRepository[%s]: %w", repo.FullName(), err) } defer closer.Close() objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) - commit, err := gitRepo.GetCommit(sha) + commit, err := gitRepo.GetCommit(ctx, sha) if err != nil { return fmt.Errorf("GetCommit[%s]: %w", sha, err) } @@ -104,7 +104,7 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato notify.CreateCommitStatus(ctx, repo, repo_module.CommitToPushCommit(commit), creator, status) - defaultBranchCommit, err := gitRepo.GetBranchCommit(repo.DefaultBranch) + defaultBranchCommit, err := gitRepo.GetBranchCommit(ctx, repo.DefaultBranch) if err != nil { return fmt.Errorf("GetBranchCommit[%s]: %w", repo.DefaultBranch, err) } diff --git a/services/repository/contributors_graph.go b/services/repository/contributors_graph.go index 503ce968c1..03219d5ed0 100644 --- a/services/repository/contributors_graph.go +++ b/services/repository/contributors_graph.go @@ -111,8 +111,8 @@ func GetContributorStats(ctx context.Context, cache cache.StringCache, repo *rep } // getExtendedCommitStats return the list of *ExtendedCommitStats for the given revision -func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int */) ([]*ExtendedCommitStats, error) { - baseCommit, err := repo.GetCommit(revision) +func getExtendedCommitStats(ctx context.Context, repo *git.Repository, revision string /*, limit int */) ([]*ExtendedCommitStats, error) { + baseCommit, err := repo.GetCommit(ctx, revision) if err != nil { return nil, err } @@ -180,7 +180,7 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int } return nil }). - RunWithStderr(repo.Ctx) + RunWithStderr(ctx) if err != nil { return nil, fmt.Errorf("ContributorsCommitStats: %w", err) } @@ -201,7 +201,7 @@ func generateContributorStats(genDone chan struct{}, cache cache.StringCache, ca if len(revision) == 0 { revision = repo.DefaultBranch } - extendedCommitStats, err := getExtendedCommitStats(gitRepo, revision) + extendedCommitStats, err := getExtendedCommitStats(ctx, gitRepo, revision) if err != nil { _ = cache.PutJSON(cacheKey, fmt.Errorf("ExtendedCommitStats: %w", err), contributorStatsCacheTimeout) return diff --git a/services/repository/create.go b/services/repository/create.go index 636f4a803b..449ab4a536 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -84,7 +84,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err) } - cloneLink := repo.CloneLink(ctx, nil /* no doer so do not generate user-related SSH link */) + cloneLink := repo.CloneLinkGeneral(ctx) match := map[string]string{ "Name": repo.Name, "Description": util.NormalizeStringEOL(repo.Description), diff --git a/services/repository/delete.go b/services/repository/delete.go index 0666a1616b..db84307610 100644 --- a/services/repository/delete.go +++ b/services/repository/delete.go @@ -179,6 +179,7 @@ func DeleteRepositoryDirectly(ctx context.Context, repoID int64, ignoreOrgTeams &actions_model.ActionArtifact{RepoID: repoID}, &actions_model.ActionRunJobSummary{RepoID: repoID}, &actions_model.ActionRunnerToken{RepoID: repoID}, + &actions_model.ActionScopedWorkflowSource{SourceRepoID: repoID}, &issues_model.IssuePin{RepoID: repoID}, ); err != nil { return fmt.Errorf("deleteBeans: %w", err) diff --git a/services/repository/files/cherry_pick.go b/services/repository/files/cherry_pick.go index 479c76b965..935fa1ee30 100644 --- a/services/repository/files/cherry_pick.go +++ b/services/repository/files/cherry_pick.go @@ -63,7 +63,7 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod } // Get the commit of the original branch - commit, err := t.GetBranchCommit(opts.OldBranch) + commit, err := t.GetBranchCommit(ctx, opts.OldBranch) if err != nil { return nil, err // Couldn't get a commit for the branch } @@ -72,7 +72,7 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod if opts.LastCommitID == "" { opts.LastCommitID = commit.ID.String() } else { - lastCommitID, err := t.gitRepo.ConvertToGitID(opts.LastCommitID) + lastCommitID, err := t.gitRepo.ConvertToGitID(ctx, opts.LastCommitID) if err != nil { return nil, fmt.Errorf("CherryPick: Invalid last commit ID: %w", err) } @@ -85,7 +85,7 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod } } - commit, err = t.GetCommit(strings.TrimSpace(opts.Content)) + commit, err = t.GetCommit(ctx, strings.TrimSpace(opts.Content)) if err != nil { return nil, err } @@ -142,12 +142,12 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod return nil, err } - commit, err = t.GetCommit(commitHash) + commit, err = t.GetCommit(ctx, commitHash) if err != nil { return nil, err } - fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil + fileCommitResponse, _ := GetFileCommitResponse(ctx, repo, gitRepo, commit) // ok if fails, then will be nil verification := GetPayloadCommitVerification(ctx, commit) fileResponse := &structs.FileResponse{ Commit: fileCommitResponse, diff --git a/services/repository/files/content.go b/services/repository/files/content.go index 42e8c193aa..e2676a757b 100644 --- a/services/repository/files/content.go +++ b/services/repository/files/content.go @@ -43,7 +43,7 @@ type GetContentsOrListOptions struct { // GetContentsOrList gets the metadata of a file's contents (*ContentsResponse) if treePath not a tree // directory, otherwise a listing of file contents ([]*ContentsResponse). Ref can be a branch, commit or tag func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, opts GetContentsOrListOptions) (ret api.ContentsExtResponse, _ error) { - entry, err := prepareGetContentsEntry(refCommit, &opts.TreePath) + entry, err := prepareGetContentsEntry(ctx, gitRepo, refCommit, &opts.TreePath) if repo.IsEmpty && opts.TreePath == "" { return api.ContentsExtResponse{DirContents: make([]*api.ContentsResponse, 0)}, nil } @@ -58,11 +58,11 @@ func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, gitRepo } // list directory contents - gitTree, err := refCommit.Commit.SubTree(opts.TreePath) + gitTree, err := refCommit.Commit.SubTree(ctx, gitRepo, opts.TreePath) if err != nil { return ret, err } - entries, err := gitTree.ListEntries() + entries, err := gitTree.ListEntries(ctx, gitRepo) if err != nil { return ret, err } @@ -96,7 +96,7 @@ func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType { } } -func prepareGetContentsEntry(refCommit *utils.RefCommit, treePath *string) (*git.TreeEntry, error) { +func prepareGetContentsEntry(ctx context.Context, gitRepo *git.Repository, refCommit *utils.RefCommit, treePath *string) (*git.TreeEntry, error) { // Check that the path given in opts.treePath is valid (not a git path) cleanTreePath := CleanGitTreePath(*treePath) if cleanTreePath == "" && *treePath != "" { @@ -110,12 +110,12 @@ func prepareGetContentsEntry(refCommit *utils.RefCommit, treePath *string) (*git return nil, util.NewNotExistErrorf("no commit found for the ref [ref: %s]", refCommit.RefName) } - return refCommit.Commit.GetTreeEntryByPath(*treePath) + return refCommit.Commit.GetTreeEntryByPath(ctx, gitRepo, *treePath) } // GetFileContents gets the metadata on a file's contents. Ref can be a branch, commit or tag func GetFileContents(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, opts GetContentsOrListOptions) (*api.ContentsResponse, error) { - entry, err := prepareGetContentsEntry(refCommit, &opts.TreePath) + entry, err := prepareGetContentsEntry(ctx, gitRepo, refCommit, &opts.TreePath) if err != nil { return nil, err } @@ -149,7 +149,7 @@ func getFileContentsByEntryInternal(ctx context.Context, repo *repo_model.Reposi Name: entry.Name(), Path: opts.TreePath, SHA: entry.ID.String(), - Size: entry.Size(), + Size: entry.GetSize(ctx, gitRepo), URL: &selfURLString, Links: &api.FileLinksResponse{ Self: &selfURLString, @@ -162,7 +162,7 @@ func getFileContentsByEntryInternal(ctx context.Context, repo *repo_model.Reposi return nil, err } - lastCommit, err := refCommit.Commit.GetCommitByPath(opts.TreePath) + lastCommit, err := refCommit.Commit.GetCommitByPath(ctx, gitRepo, opts.TreePath) if err != nil { return nil, err } @@ -188,14 +188,14 @@ func getFileContentsByEntryInternal(ctx context.Context, repo *repo_model.Reposi contentsResponse.Type = string(ContentTypeRegular) // if it is listing the repo root dir, don't waste system resources on reading content if opts.IncludeSingleFileContent { - blobResponse, err := GetBlobBySHA(repo, gitRepo, entry.ID.String()) + blobResponse, err := GetBlobBySHA(ctx, repo, gitRepo, entry.ID.String()) if err != nil { return nil, err } contentsResponse.Encoding, contentsResponse.Content = blobResponse.Encoding, blobResponse.Content contentsResponse.LfsOid, contentsResponse.LfsSize = blobResponse.LfsOid, blobResponse.LfsSize } else if opts.IncludeLfsMetadata { - contentsResponse.LfsOid, contentsResponse.LfsSize, err = parsePossibleLfsPointerBlob(gitRepo, entry.ID.String()) + contentsResponse.LfsOid, contentsResponse.LfsSize, err = parsePossibleLfsPointerBlob(ctx, gitRepo, entry.ID.String()) if err != nil { return nil, err } @@ -205,14 +205,14 @@ func getFileContentsByEntryInternal(ctx context.Context, repo *repo_model.Reposi } else if entry.IsLink() { contentsResponse.Type = string(ContentTypeLink) // The target of a symlink file is the content of the file - targetFromContent, err := entry.Blob().GetBlobContent(1024) + targetFromContent, err := entry.Blob(gitRepo).GetBlobContent(ctx, 1024) if err != nil { return nil, err } contentsResponse.Target = &targetFromContent } else if entry.IsSubModule() { contentsResponse.Type = string(ContentTypeSubmodule) - submodule, err := commit.GetSubModule(opts.TreePath) + submodule, err := commit.GetSubModule(ctx, gitRepo, opts.TreePath) if err != nil { return nil, err } @@ -250,7 +250,7 @@ func getFileContentsByEntryInternal(ctx context.Context, repo *repo_model.Reposi return contentsResponse, nil } -func GetBlobBySHA(repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlobResponse, error) { +func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlobResponse, error) { gitBlob, err := gitRepo.GetBlob(sha) if err != nil { return nil, err @@ -258,10 +258,10 @@ func GetBlobBySHA(repo *repo_model.Repository, gitRepo *git.Repository, sha stri ret := &api.GitBlobResponse{ SHA: gitBlob.ID.String(), URL: repo.APIURL() + "/git/blobs/" + url.PathEscape(gitBlob.ID.String()), - Size: gitBlob.Size(), + Size: gitBlob.Size(ctx), } - blobSize := gitBlob.Size() + blobSize := gitBlob.Size(ctx) if blobSize > setting.API.DefaultMaxBlobSize { return ret, nil } @@ -271,7 +271,7 @@ func GetBlobBySHA(repo *repo_model.Repository, gitRepo *git.Repository, sha stri originContent = &strings.Builder{} } - content, err := gitBlob.GetBlobContentBase64(originContent) + content, err := gitBlob.GetBlobContentBase64(ctx, originContent) if err != nil { return nil, err } @@ -291,15 +291,15 @@ func parsePossibleLfsPointerBuffer(r io.Reader) (*string, *int64) { return nil, nil } -func parsePossibleLfsPointerBlob(gitRepo *git.Repository, sha string) (*string, *int64, error) { +func parsePossibleLfsPointerBlob(ctx context.Context, gitRepo *git.Repository, sha string) (*string, *int64, error) { gitBlob, err := gitRepo.GetBlob(sha) if err != nil { return nil, nil, err } - if gitBlob.Size() > lfs.MetaFileMaxSize { + if gitBlob.Size(ctx) > lfs.MetaFileMaxSize { return nil, nil, nil // not a LFS pointer } - buf, err := gitBlob.GetBlobContent(lfs.MetaFileMaxSize) + buf, err := gitBlob.GetBlobContent(ctx, lfs.MetaFileMaxSize) if err != nil { return nil, nil, err } diff --git a/services/repository/files/content_test.go b/services/repository/files/content_test.go index 52b7e8a013..ea4e3c4afe 100644 --- a/services/repository/files/content_test.go +++ b/services/repository/files/content_test.go @@ -34,7 +34,7 @@ func TestGetContents(t *testing.T) { sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" ctx.SetPathParam("id", "1") ctx.SetPathParam("sha", sha) - gbr, err := GetBlobBySHA(ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("sha")) + gbr, err := GetBlobBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("sha")) expectedGBR := &api.GitBlobResponse{ Content: new("dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK"), Encoding: new("base64"), diff --git a/services/repository/files/file.go b/services/repository/files/file.go index df88ee4d89..7db74dba8b 100644 --- a/services/repository/files/file.go +++ b/services/repository/files/file.go @@ -45,7 +45,7 @@ func GetContentsListFromTreePaths(ctx context.Context, repo *repo_model.Reposito func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, treeNames []string) (*api.FilesResponse, error) { files := GetContentsListFromTreePaths(ctx, repo, gitRepo, refCommit, treeNames) - fileCommitResponse, _ := GetFileCommitResponse(repo, refCommit.Commit) // ok if fails, then will be nil + fileCommitResponse, _ := GetFileCommitResponse(ctx, repo, gitRepo, refCommit.Commit) // ok if fails, then will be nil verification := GetPayloadCommitVerification(ctx, refCommit.Commit) filesResponse := &api.FilesResponse{ Files: files, @@ -70,7 +70,7 @@ func GetFileResponseFromFilesResponse(filesResponse *api.FilesResponse, index in } // GetFileCommitResponse Constructs a FileCommitResponse from a Commit object -func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*api.FileCommitResponse, error) { +func GetFileCommitResponse(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit) (*api.FileCommitResponse, error) { if repo == nil { return nil, errors.New("repo cannot be nil") } @@ -78,10 +78,10 @@ func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*ap return nil, errors.New("commit cannot be nil") } commitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String())) - commitTreeURL, _ := url.Parse(repo.APIURL() + "/git/trees/" + url.PathEscape(commit.Tree.ID.String())) + commitTreeURL, _ := url.Parse(repo.APIURL() + "/git/trees/" + url.PathEscape(commit.TreeID.String())) parents := make([]*api.CommitMeta, commit.ParentCount()) - for i := 0; i <= commit.ParentCount(); i++ { - if parent, err := commit.Parent(i); err == nil && parent != nil { + for i := 0; i < commit.ParentCount(); i++ { + if parent, err := commit.Parent(ctx, gitRepo, i); err == nil && parent != nil { parentCommitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + url.PathEscape(parent.ID.String())) parents[i] = &api.CommitMeta{ SHA: parent.ID.String(), @@ -113,7 +113,7 @@ func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*ap Message: commit.MessageUTF8(), Tree: &api.CommitMeta{ URL: commitTreeURL.String(), - SHA: commit.Tree.ID.String(), + SHA: commit.TreeID.String(), }, Parents: parents, } diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go index 8c797a24ec..c18dacea6e 100644 --- a/services/repository/files/patch.go +++ b/services/repository/files/patch.go @@ -142,7 +142,7 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user } // Get the commit of the original branch - commit, err := t.GetBranchCommit(opts.OldBranch) + commit, err := t.GetBranchCommit(ctx, opts.OldBranch) if err != nil { return nil, err // Couldn't get a commit for the branch } @@ -151,7 +151,7 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user if opts.LastCommitID == "" { opts.LastCommitID = commit.ID.String() } else { - lastCommitID, err := t.gitRepo.ConvertToGitID(opts.LastCommitID) + lastCommitID, err := t.gitRepo.ConvertToGitID(ctx, opts.LastCommitID) if err != nil { return nil, fmt.Errorf("ApplyPatch: Invalid last commit ID: %w", err) } @@ -206,12 +206,12 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user return nil, err } - commit, err = t.GetCommit(commitHash) + commit, err = t.GetCommit(ctx, commitHash) if err != nil { return nil, err } - fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil + fileCommitResponse, _ := GetFileCommitResponse(ctx, repo, gitRepo, commit) // ok if fails, then will be nil verification := GetPayloadCommitVerification(ctx, commit) fileResponse := &structs.FileResponse{ Commit: fileCommitResponse, diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index bffd352583..f3db222171 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -48,7 +48,10 @@ func NewTemporaryUploadRepository(repo *repo_model.Repository) (*TemporaryUpload // Close the repository cleaning up all files func (t *TemporaryUploadRepository) Close() { // must stop the repo access before removal, otherwise Windows can't remove the directory occupied by other processes - t.gitRepo.Close() + if t.gitRepo != nil { + _ = t.gitRepo.Close() + t.gitRepo = nil + } if t.cleanup != nil { t.cleanup() } @@ -76,7 +79,7 @@ func (t *TemporaryUploadRepository) Clone(ctx context.Context, branch string, ba } return fmt.Errorf("Clone: %w %s", err, stderr) } - gitRepo, err := git.OpenRepository(ctx, t.basePath) + gitRepo, err := git.OpenRepositoryLocal(t.basePath) if err != nil { return err } @@ -89,7 +92,7 @@ func (t *TemporaryUploadRepository) Init(ctx context.Context, objectFormatName s if err := git.InitRepository(ctx, t.basePath, false, objectFormatName); err != nil { return err } - gitRepo, err := git.OpenRepository(ctx, t.basePath) + gitRepo, err := git.OpenRepositoryLocal(t.basePath) if err != nil { return err } @@ -141,7 +144,7 @@ func (t *TemporaryUploadRepository) RemoveRecursivelyFromIndex(ctx context.Conte // RemoveFilesFromIndex removes the given files from the index func (t *TemporaryUploadRepository) RemoveFilesFromIndex(ctx context.Context, filenames ...string) error { - objFmt, err := t.gitRepo.GetObjectFormat() + objFmt, err := t.gitRepo.GetObjectFormat(ctx) if err != nil { return fmt.Errorf("unable to get object format for temporary repo: %q, error: %w", t.repo.FullName(), err) } @@ -387,17 +390,17 @@ func (t *TemporaryUploadRepository) DiffIndex(ctx context.Context, oldContent, n } // GetBranchCommit Gets the commit object of the given branch -func (t *TemporaryUploadRepository) GetBranchCommit(branch string) (*git.Commit, error) { +func (t *TemporaryUploadRepository) GetBranchCommit(ctx context.Context, branch string) (*git.Commit, error) { if t.gitRepo == nil { return nil, errors.New("repository has not been cloned") } - return t.gitRepo.GetBranchCommit(branch) + return t.gitRepo.GetBranchCommit(ctx, branch) } // GetCommit Gets the commit object of the given commit ID -func (t *TemporaryUploadRepository) GetCommit(commitID string) (*git.Commit, error) { +func (t *TemporaryUploadRepository) GetCommit(ctx context.Context, commitID string) (*git.Commit, error) { if t.gitRepo == nil { return nil, errors.New("repository has not been cloned") } - return t.gitRepo.GetCommit(commitID) + return t.gitRepo.GetCommit(ctx, commitID) } diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go index 7b822db0ab..9de8516eed 100644 --- a/services/repository/files/tree.go +++ b/services/repository/files/tree.go @@ -22,35 +22,20 @@ import ( "gitea.dev/modules/util" ) -// ErrSHANotFound represents a "SHADoesNotMatch" kind of error. -type ErrSHANotFound struct { - SHA string -} - -func (err ErrSHANotFound) Error() string { - return fmt.Sprintf("sha not found [%s]", err.SHA) -} - -func (err ErrSHANotFound) Unwrap() error { - return util.ErrNotExist -} - -// GetTreeBySHA get the GitTreeResponse of a repository using a sha hash. +// GetTreeBySHA get the GitTreeResponse of a repository using a sha hash (id of a commit or a tree) func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, sha string, page, perPage int, recursive bool) (*api.GitTreeResponse, error) { - gitTree, err := gitRepo.GetTree(sha) - if err != nil || gitTree == nil { - return nil, ErrSHANotFound{ // TODO: this error has never been catch outside of this function - SHA: sha, - } + gitTree, err := gitRepo.GetTree(ctx, sha) + if err != nil { + return nil, util.NewInvalidArgumentErrorf("sha not found [%s]", sha) } tree := new(api.GitTreeResponse) - tree.SHA = gitTree.ResolvedID.String() - tree.URL = repo.APIURL() + "/git/trees/" + url.PathEscape(tree.SHA) + tree.SHA = gitTree.ID.String() // always return the real tree id to end users, but not the commit's id if sha is a commit + tree.URL = repo.APIURL(ctx) + "/git/trees/" + url.PathEscape(tree.SHA) var entries git.Entries if recursive { - entries, err = gitTree.ListEntriesRecursiveWithSize() + entries, err = gitTree.ListEntriesRecursiveWithSize(ctx, gitRepo) } else { - entries, err = gitTree.ListEntries() + entries, err = gitTree.ListEntries(ctx, gitRepo) } if err != nil { return nil, err @@ -79,7 +64,7 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git tree.Entries[i].Path = entries[e].Name() tree.Entries[i].Mode = fmt.Sprintf("%06o", entries[e].Mode()) tree.Entries[i].Type = entries[e].Type() - tree.Entries[i].Size = entries[e].Size() + tree.Entries[i].Size = entries[e].GetSize(ctx, gitRepo) tree.Entries[i].SHA = entries[e].ID.String() if entries[e].IsDir() { @@ -129,14 +114,14 @@ func (node *TreeViewNode) sortLevel() int { return util.Iif(node.EntryMode == "tree" || node.EntryMode == "commit", 0, 1) } -func newTreeViewNodeFromEntry(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode { +func newTreeViewNodeFromEntry(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, gitRepo *git.Repository, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode { node := &TreeViewNode{ EntryName: entry.Name(), EntryMode: entryModeString(entry.Mode()), FullPath: path.Join(parentDir, entry.Name()), } - entryInfo := fileicon.EntryInfoFromGitTreeEntry(commit, node.FullPath, entry) + entryInfo := fileicon.EntryInfoFromGitTreeEntry(ctx, gitRepo, commit, node.FullPath, entry) node.EntryIcon = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo) if entryInfo.EntryMode.IsDir() { entryInfo.IsOpen = true @@ -144,7 +129,7 @@ func newTreeViewNodeFromEntry(ctx context.Context, repoLink string, renderedIcon } if node.EntryMode == "commit" { - if subModule, err := commit.GetSubModule(node.FullPath); err != nil { + if subModule, err := commit.GetSubModule(ctx, gitRepo, node.FullPath); err != nil { log.Error("GetSubModule: %v", err) } else if subModule != nil { submoduleFile := git.NewCommitSubmoduleFile(repoLink, node.FullPath, subModule.URL, entry.ID.String()) @@ -169,8 +154,8 @@ func sortTreeViewNodes(nodes []*TreeViewNode) { }) } -func listTreeNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) { - entries, err := tree.ListEntries() +func listTreeNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, gitRepo *git.Repository, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) { + entries, err := tree.ListEntries(ctx, gitRepo) if err != nil { return nil, err } @@ -178,14 +163,14 @@ func listTreeNodes(ctx context.Context, repoLink string, renderedIconPool *filei subPathDirName, subPathRemaining, _ := strings.Cut(subPath, "/") nodes := make([]*TreeViewNode, 0, len(entries)) for _, entry := range entries { - node := newTreeViewNodeFromEntry(ctx, repoLink, renderedIconPool, commit, treePath, entry) + node := newTreeViewNodeFromEntry(ctx, repoLink, renderedIconPool, gitRepo, commit, treePath, entry) nodes = append(nodes, node) if entry.IsDir() && subPathDirName == entry.Name() { subTreePath := treePath + "/" + node.EntryName if subTreePath[0] == '/' { subTreePath = subTreePath[1:] } - subNodes, err := listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), subTreePath, subPathRemaining) + subNodes, err := listTreeNodes(ctx, repoLink, renderedIconPool, gitRepo, commit, entry.Tree(ctx, gitRepo), subTreePath, subPathRemaining) if err != nil { log.Error("listTreeNodes: %v", err) } else { @@ -197,10 +182,10 @@ func listTreeNodes(ctx context.Context, repoLink string, renderedIconPool *filei return nodes, nil } -func GetTreeViewNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) { - entry, err := commit.GetTreeEntryByPath(treePath) +func GetTreeViewNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, gitRepo *git.Repository, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) { + entry, err := commit.GetTreeEntryByPath(ctx, gitRepo, treePath) if err != nil { return nil, err } - return listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), treePath, subPath) + return listTreeNodes(ctx, repoLink, renderedIconPool, gitRepo, commit, entry.Tree(ctx, gitRepo), treePath, subPath) } diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go index fe42bdb054..ea67e1248f 100644 --- a/services/repository/files/tree_test.go +++ b/services/repository/files/tree_test.go @@ -25,17 +25,15 @@ func TestGetTreeBySHA(t *testing.T) { contexttest.LoadGitRepo(t, ctx) defer ctx.Repo.GitRepo.Close() - sha := ctx.Repo.Repository.DefaultBranch page := 1 perPage := 10 - ctx.SetPathParam("id", "1") - ctx.SetPathParam("sha", sha) + ctx.SetPathParam("sha", ctx.Repo.Repository.DefaultBranch) tree, err := GetTreeBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("sha"), page, perPage, true) assert.NoError(t, err) expectedTree := &api.GitTreeResponse{ - SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", - URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/trees/65f1bf27bc3bf70f64657658635e66094edbcb4d", + SHA: "2a2f1d4670728a2e10049e345bd7a276468beab6", + URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/trees/2a2f1d4670728a2e10049e345bd7a276468beab6", Entries: []api.GitEntry{ { Path: "README.md", @@ -78,7 +76,7 @@ func TestGetTreeViewNodes(t *testing.T) { // With basic theme (default for folders), we get octicon icons without IDs return template.HTML(`octicon-file-directory-open-fill(16/)`) } - treeNodes, err := GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "", "") + treeNodes, err := GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.GitRepo, ctx.Repo.Commit, "", "") assert.NoError(t, err) assert.Equal(t, []*TreeViewNode{ { @@ -90,7 +88,7 @@ func TestGetTreeViewNodes(t *testing.T) { }, }, treeNodes) - treeNodes, err = GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "", "docs/README.md") + treeNodes, err = GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.GitRepo, ctx.Repo.Commit, "", "docs/README.md") assert.NoError(t, err) assert.Equal(t, []*TreeViewNode{ { @@ -110,7 +108,7 @@ func TestGetTreeViewNodes(t *testing.T) { }, }, treeNodes) - treeNodes, err = GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "docs", "README.md") + treeNodes, err = GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.GitRepo, ctx.Repo.Commit, "docs", "README.md") assert.NoError(t, err) assert.Equal(t, []*TreeViewNode{ { diff --git a/services/repository/files/update.go b/services/repository/files/update.go index 8da9021a40..33cd2bf682 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -202,7 +202,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use if hasOldBranch { // Get the commit of the original branch - commit, err := t.GetBranchCommit(opts.OldBranch) + commit, err := t.GetBranchCommit(ctx, opts.OldBranch) if err != nil { return nil, err // Couldn't get a commit for the branch } @@ -211,7 +211,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use if opts.LastCommitID == "" { opts.LastCommitID = commit.ID.String() } else { - lastCommitID, err := t.gitRepo.ConvertToGitID(opts.LastCommitID) + lastCommitID, err := t.gitRepo.ConvertToGitID(ctx, opts.LastCommitID) if err != nil { return nil, fmt.Errorf("ConvertToSHA1: Invalid last commit ID: %w", err) } @@ -219,7 +219,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } for _, file := range opts.Files { - if err = handleCheckErrors(file, commit, opts); err != nil { + if err = handleCheckErrors(ctx, file, t.gitRepo, commit, opts); err != nil { return nil, err } } @@ -282,7 +282,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use return nil, err } - commit, err := t.GetCommit(commitHash) + commit, err := t.GetCommit(ctx, commitHash) if err != nil { return nil, err } @@ -295,7 +295,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } if repo.IsEmpty { - if isEmpty, err := gitRepo.IsEmpty(); err == nil && !isEmpty { + if isEmpty, err := gitRepo.IsEmpty(ctx); err == nil && !isEmpty { _ = repo_model.UpdateRepositoryColsWithAutoTime(ctx, &repo_model.Repository{ID: repo.ID, IsEmpty: false, DefaultBranch: opts.NewBranch}, "is_empty", "default_branch") } } @@ -361,12 +361,12 @@ func (err ErrSHAOrCommitIDNotProvided) Error() string { } // handles the check for various issues for ChangeRepoFiles -func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions) error { +func handleCheckErrors(ctx context.Context, file *ChangeRepoFile, gitRepo *git.Repository, commit *git.Commit, opts *ChangeRepoFilesOptions) error { // check old entry (fromTreePath/fromEntry) if file.Operation == "update" || file.Operation == "upload" || file.Operation == "delete" || file.Operation == "rename" { var fromEntryIDString string { - fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath) + fromEntry, err := commit.GetTreeEntryByPath(ctx, gitRepo, file.Options.fromTreePath) if file.Operation == "upload" && git.IsErrNotExist(err) { fromEntry = nil } else if err != nil { @@ -391,7 +391,7 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep // If a lastCommitID given doesn't match the branch head's commitID throw // an error, but only if we aren't creating a new branch. if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch { - if changed, err := commit.FileChangedSinceCommit(file.Options.treePath, opts.LastCommitID); err != nil { + if changed, err := commit.FileChangedSinceCommit(ctx, gitRepo, file.Options.treePath, opts.LastCommitID); err != nil { return err } else if changed { return ErrCommitIDDoesNotMatch{ @@ -417,7 +417,7 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep subTreePath := "" for index, part := range treePathParts { subTreePath = path.Join(subTreePath, part) - entry, err := commit.GetTreeEntryByPath(subTreePath) + entry, err := commit.GetTreeEntryByPath(ctx, gitRepo, subTreePath) if err != nil { if git.IsErrNotExist(err) { // Means there is no item with that name, so we're good @@ -592,11 +592,11 @@ func writeRepoObjectForRename(ctx context.Context, t *TemporaryUploadRepository, if err != nil { return nil, err } - commit, err := t.GetCommit(lastCommitID) + commit, err := t.GetCommit(ctx, lastCommitID) if err != nil { return nil, err } - oldEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath) + oldEntry, err := commit.GetTreeEntryByPath(ctx, t.gitRepo, file.Options.fromTreePath) if err != nil { return nil, err } @@ -619,7 +619,7 @@ func writeRepoObjectForRename(ctx context.Context, t *TemporaryUploadRepository, } oldEntryBlobPointerBy := func(f func(r io.Reader) (lfs.Pointer, error)) (lfsPointer lfs.Pointer, err error) { - r, err := oldEntry.Blob().DataAsync() + r, err := oldEntry.Blob(t.gitRepo).DataAsync(ctx) if err != nil { return lfsPointer, err } @@ -645,7 +645,7 @@ func writeRepoObjectForRename(ctx context.Context, t *TemporaryUploadRepository, if err != nil { return nil, err } - ret.LfsContent, err = oldEntry.Blob().DataAsync() + ret.LfsContent, err = oldEntry.Blob(t.gitRepo).DataAsync(ctx) if err != nil { return nil, err } diff --git a/services/repository/fork.go b/services/repository/fork.go index a469836da0..1697548541 100644 --- a/services/repository/fork.go +++ b/services/repository/fork.go @@ -171,7 +171,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork // 6 - Sync the repository branches and tags var gitRepo *git.Repository - gitRepo, err = gitrepo.OpenRepository(ctx, repo) + gitRepo, err = gitrepo.OpenRepository(repo) if err != nil { return nil, fmt.Errorf("OpenRepository: %w", err) } diff --git a/services/repository/gitgraph/graph.go b/services/repository/gitgraph/graph.go index ab0203f4d5..c744a13de2 100644 --- a/services/repository/gitgraph/graph.go +++ b/services/repository/gitgraph/graph.go @@ -6,6 +6,7 @@ package gitgraph import ( "bufio" "bytes" + "context" "gitea.dev/modules/git" "gitea.dev/modules/git/gitcmd" @@ -13,7 +14,7 @@ import ( ) // GetCommitGraph return a list of commit (GraphItems) from all branches -func GetCommitGraph(r *git.Repository, page, maxAllowedColors int, hidePRRefs bool, branches, files []string) (*Graph, error) { +func GetCommitGraph(ctx context.Context, r *git.Repository, page, maxAllowedColors int, hidePRRefs bool, branches, files []string) (*Graph, error) { format := "DATA:%D|%H|%ad|%h|%s" if page == 0 { @@ -97,7 +98,7 @@ func GetCommitGraph(r *git.Repository, page, maxAllowedColors int, hidePRRefs bo } return scanner.Err() }). - RunWithStderr(r.Ctx); err != nil { + RunWithStderr(ctx); err != nil { return graph, err } return graph, nil diff --git a/services/repository/gitgraph/graph_models.go b/services/repository/gitgraph/graph_models.go index 99f8222ca7..5ba47c5122 100644 --- a/services/repository/gitgraph/graph_models.go +++ b/services/repository/gitgraph/graph_models.go @@ -102,7 +102,7 @@ func (graph *Graph) LoadAndProcessCommits(ctx context.Context, repository *repo_ if len(c.Rev) == 0 { continue } - c.Commit, err = gitRepo.GetCommit(c.Rev) + c.Commit, err = gitRepo.GetCommit(ctx, c.Rev) if err != nil { return fmt.Errorf("GetCommit: %s Error: %w", c.Rev, err) } diff --git a/services/repository/gitgraph/graph_test.go b/services/repository/gitgraph/graph_test.go index a6cc0ae048..5ebfddf368 100644 --- a/services/repository/gitgraph/graph_test.go +++ b/services/repository/gitgraph/graph_test.go @@ -16,14 +16,14 @@ import ( ) func BenchmarkGetCommitGraph(b *testing.B) { - currentRepo, err := git.OpenRepository(b.Context(), ".") + currentRepo, err := git.OpenRepositoryLocal(".") if err != nil || currentRepo == nil { b.Error("Could not open repository") } defer currentRepo.Close() for b.Loop() { - graph, err := GetCommitGraph(currentRepo, 1, 0, false, nil, nil) + graph, err := GetCommitGraph(b.Context(), currentRepo, 1, 0, false, nil, nil) if err != nil { b.Error("Could get commit graph") } diff --git a/services/repository/hooks.go b/services/repository/hooks.go index c75c66af1e..919781cabe 100644 --- a/services/repository/hooks.go +++ b/services/repository/hooks.go @@ -52,13 +52,13 @@ func SyncRepositoryHooks(ctx context.Context) error { // GenerateGitHooks generates git hooks from a template repository func GenerateGitHooks(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) error { - generateGitRepo, err := gitrepo.OpenRepository(ctx, generateRepo) + generateGitRepo, err := gitrepo.OpenRepository(generateRepo) if err != nil { return err } defer generateGitRepo.Close() - templateGitRepo, err := gitrepo.OpenRepository(ctx, templateRepo) + templateGitRepo, err := gitrepo.OpenRepository(templateRepo) if err != nil { return err } diff --git a/services/repository/lfs.go b/services/repository/lfs.go index 08a5ab494e..0de365864d 100644 --- a/services/repository/lfs.go +++ b/services/repository/lfs.go @@ -70,7 +70,7 @@ func GarbageCollectLFSMetaObjectsForRepo(ctx context.Context, repo *repo_model.R } }() - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + gitRepo, err := gitrepo.OpenRepository(repo) if err != nil { log.Error("Unable to open git repository %-v: %v", repo, err) return err @@ -88,7 +88,7 @@ func GarbageCollectLFSMetaObjectsForRepo(ctx context.Context, repo *repo_model.R total++ pointerSha := git.ComputeBlobHash(objectFormat, []byte(metaObject.Pointer.StringContent())) - if gitRepo.IsObjectExist(pointerSha.String()) { + if gitRepo.IsObjectExist(ctx, pointerSha.String()) { return git_model.MarkLFSMetaObject(ctx, metaObject.ID) } orphaned++ diff --git a/services/repository/license.go b/services/repository/license.go index 61c58e7282..95059e1d01 100644 --- a/services/repository/license.go +++ b/services/repository/license.go @@ -72,19 +72,19 @@ func repoLicenseUpdater(items ...*LicenseUpdaterOptions) []*LicenseUpdaterOption continue } - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + gitRepo, err := gitrepo.OpenRepository(repo) if err != nil { log.Error("repoLicenseUpdater [%d] failed: OpenRepository: %v", opts.RepoID, err) continue } defer gitRepo.Close() - commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch) + commit, err := gitRepo.GetBranchCommit(ctx, repo.DefaultBranch) if err != nil { log.Error("repoLicenseUpdater [%d] failed: GetBranchCommit: %v", opts.RepoID, err) continue } - if err = UpdateRepoLicenses(ctx, repo, commit); err != nil { + if err = UpdateRepoLicenses(ctx, repo, gitRepo, commit); err != nil { log.Error("repoLicenseUpdater [%d] failed: updateRepoLicenses: %v", opts.RepoID, err) } } @@ -115,12 +115,12 @@ func SyncRepoLicenses(ctx context.Context) error { } // UpdateRepoLicenses will update repository licenses col if license file exists -func UpdateRepoLicenses(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) error { +func UpdateRepoLicenses(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit) error { if commit == nil { return nil } - b, err := commit.GetBlobByPath(LicenseFileName) + b, err := commit.GetBlobByPath(ctx, gitRepo, LicenseFileName) if err != nil && !git.IsErrNotExist(err) { return fmt.Errorf("GetBlobByPath: %w", err) } @@ -131,7 +131,7 @@ func UpdateRepoLicenses(ctx context.Context, repo *repo_model.Repository, commit licenses := make([]string, 0) if b != nil { - r, err := b.DataAsync() + r, err := b.DataAsync(ctx) if err != nil { return err } diff --git a/services/repository/migrate.go b/services/repository/migrate.go index c328d070c8..2aca0666d5 100644 --- a/services/repository/migrate.go +++ b/services/repository/migrate.go @@ -36,12 +36,12 @@ func cloneWiki(ctx context.Context, repo *repo_model.Repository, opts migration. storageRepo := repo.WikiStorageRepo() if err := gitrepo.DeleteRepository(ctx, storageRepo); err != nil { - return "", fmt.Errorf("failed to remove existing wiki dir %q, err: %w", storageRepo.RelativePath(), err) + return "", fmt.Errorf("failed to remove existing wiki for repo %q, err: %w", repo.FullName(), err) } cleanIncompleteWikiPath := func() { if err := gitrepo.DeleteRepository(ctx, storageRepo); err != nil { - log.Error("Failed to remove incomplete wiki dir %q, err: %v", storageRepo.RelativePath(), err) + log.Error("Failed to remove incomplete wiki for repo %q, err: %v", repo.FullName(), err) } } if err := gitrepo.CloneExternalRepo(ctx, wikiRemoteURL, storageRepo, git.CloneRepoOptions{ @@ -55,7 +55,7 @@ func cloneWiki(ctx context.Context, repo *repo_model.Repository, opts migration. return "", err } - if err := gitrepo.WriteCommitGraph(ctx, storageRepo); err != nil { + if err := git.WriteCommitGraph(ctx, storageRepo); err != nil { cleanIncompleteWikiPath() return "", err } @@ -63,7 +63,7 @@ func cloneWiki(ctx context.Context, repo *repo_model.Repository, opts migration. defaultBranch, err := gitrepo.GetDefaultBranch(ctx, storageRepo) if err != nil { cleanIncompleteWikiPath() - return "", fmt.Errorf("failed to get wiki repo default branch for %q, err: %w", storageRepo.RelativePath(), err) + return "", fmt.Errorf("failed to get wiki repo default branch for %q, err: %w", repo.FullName(), err) } return defaultBranch, nil @@ -102,7 +102,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, return repo, fmt.Errorf("clone error: %w", err) } - if err := gitrepo.WriteCommitGraph(ctx, repo); err != nil { + if err := git.WriteCommitGraph(ctx, repo); err != nil { return repo, err } @@ -122,13 +122,13 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, return nil, fmt.Errorf("updateGitRepoAfterCreate: %w", err) } - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + gitRepo, err := gitrepo.OpenRepository(repo) if err != nil { return repo, fmt.Errorf("OpenRepository: %w", err) } defer gitRepo.Close() - repo.IsEmpty, err = gitRepo.IsEmpty() + repo.IsEmpty, err = gitRepo.IsEmpty(ctx) if err != nil { return repo, fmt.Errorf("git.IsEmpty: %w", err) } diff --git a/services/repository/push.go b/services/repository/push.go index 7666ecf377..ae9963143f 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -74,7 +74,7 @@ func pushQueueHandleUpdates(optsList []*repo_module.PushUpdateOptions) error { return fmt.Errorf("GetRepositoryByOwnerAndName failed: %w", err) } - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + gitRepo, err := gitrepo.OpenRepository(repo) if err != nil { return fmt.Errorf("OpenRepository[%s]: %w", repo.FullName(), err) } @@ -120,11 +120,11 @@ func pushQueueHandleUpdates(optsList []*repo_module.PushUpdateOptions) error { delTags = append(delTags, tagName) notify_service.DeleteRef(ctx, pusher, repo, opts.RefFullName) } else { // is new tag - newCommit, err := gitRepo.GetCommit(opts.NewCommitID) + newCommit, err := gitRepo.GetCommit(ctx, opts.NewCommitID) if err != nil { // in case there is dirty data, for example, the "github.com/git/git" repository has tags pointing to non-existing commits if !errors.Is(err, util.ErrNotExist) { - log.Error("Unable to get tag commit: gitRepo.GetCommit(%s) in %s/%s[%d]: %v", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err) + log.Error("Unable to get tag commit: gitRepo.GetCommit(ctx, %s) in %s/%s[%d]: %v", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err) } } else { commits := repo_module.NewPushCommits() @@ -160,17 +160,17 @@ func pushQueueHandleUpdates(optsList []*repo_module.PushUpdateOptions) error { log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) - newCommit, err := gitRepo.GetCommit(opts.NewCommitID) + newCommit, err := gitRepo.GetCommit(ctx, opts.NewCommitID) if err != nil { - return fmt.Errorf("gitRepo.GetCommit(%s) in %s/%s[%d]: %w", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err) + return fmt.Errorf("gitRepo.GetCommit(ctx, %s) in %s/%s[%d]: %w", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err) } // Push new branch. var l []*git.Commit if opts.IsNewRef() { - l, err = pushNewBranch(ctx, repo, pusher, opts, newCommit) + l, err = pushNewBranch(ctx, repo, gitRepo, pusher, opts, newCommit) } else { - l, err = pushUpdateBranch(ctx, repo, pusher, opts, newCommit) + l, err = pushUpdateBranch(ctx, repo, gitRepo, pusher, opts, newCommit) } if err != nil { return err @@ -197,7 +197,7 @@ func pushQueueHandleUpdates(optsList []*repo_module.PushUpdateOptions) error { log.Error("updateIssuesCommit: %v", err) } - commits.CompareURL = getCompareURL(repo, gitRepo, objectFormat, commits.Commits, opts) + commits.CompareURL = getCompareURL(ctx, repo, gitRepo, objectFormat, commits.Commits, opts) if len(commits.Commits) > setting.UI.FeedMaxCommitNum { commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum] @@ -236,10 +236,10 @@ func pushQueueHandleUpdates(optsList []*repo_module.PushUpdateOptions) error { return nil } -func getCompareURL(repo *repo_model.Repository, gitRepo *git.Repository, objectFormat git.ObjectFormat, commits []*repo_module.PushCommit, opts *repo_module.PushUpdateOptions) string { +func getCompareURL(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, objectFormat git.ObjectFormat, commits []*repo_module.PushCommit, opts *repo_module.PushUpdateOptions) string { oldCommitID := opts.OldCommitID if oldCommitID == objectFormat.EmptyObjectID().String() && len(commits) > 0 { - oldCommit, err := gitRepo.GetCommit(commits[len(commits)-1].Sha1) + oldCommit, err := gitRepo.GetCommit(ctx, commits[len(commits)-1].Sha1) if err != nil && !git.IsErrNotExist(err) { log.Error("unable to GetCommit %s from %-v: %v", oldCommitID, repo, err) } @@ -264,7 +264,7 @@ func getCompareURL(repo *repo_model.Repository, gitRepo *git.Repository, objectF return "" } -func pushNewBranch(ctx context.Context, repo *repo_model.Repository, pusher *user_model.User, opts *repo_module.PushUpdateOptions, newCommit *git.Commit) ([]*git.Commit, error) { +func pushNewBranch(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, pusher *user_model.User, opts *repo_module.PushUpdateOptions, newCommit *git.Commit) ([]*git.Commit, error) { if repo.IsEmpty { // Change default branch and empty status only if pushed ref is non-empty branch. repo.DefaultBranch = opts.RefName() repo.IsEmpty = false @@ -279,7 +279,7 @@ func pushNewBranch(ctx context.Context, repo *repo_model.Repository, pusher *use } } - l, err := newCommit.CommitsBeforeLimit(10) + l, err := newCommit.CommitsBeforeLimit(ctx, gitRepo, 10) if err != nil { return nil, fmt.Errorf("newCommit.CommitsBeforeLimit: %w", err) } @@ -287,15 +287,15 @@ func pushNewBranch(ctx context.Context, repo *repo_model.Repository, pusher *use return l, nil } -func pushUpdateBranch(_ context.Context, repo *repo_model.Repository, pusher *user_model.User, opts *repo_module.PushUpdateOptions, newCommit *git.Commit) ([]*git.Commit, error) { - l, err := newCommit.CommitsBeforeUntil(git.RefNameFromCommit(opts.OldCommitID)) +func pushUpdateBranch(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, pusher *user_model.User, opts *repo_module.PushUpdateOptions, newCommit *git.Commit) ([]*git.Commit, error) { + l, err := newCommit.CommitsBeforeUntil(ctx, gitRepo, git.RefNameFromCommit(opts.OldCommitID)) if err != nil { return nil, fmt.Errorf("newCommit.CommitsBeforeUntil: %w", err) } branch := opts.RefFullName.BranchName() - isForcePush, err := newCommit.IsForcePush(opts.OldCommitID) + isForcePush, err := newCommit.IsForcePush(ctx, gitRepo, opts.OldCommitID) if err != nil { log.Error("IsForcePush %s:%s failed: %v", repo.FullName(), branch, err) } @@ -370,11 +370,11 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo newReleases := make([]*repo_model.Release, 0, len(lowerTags)-len(relMap)) for i, lowerTag := range lowerTags { - tag, err := gitRepo.GetTag(tags[i]) + tag, err := gitRepo.GetTag(ctx, tags[i]) if err != nil { return fmt.Errorf("GetTag: %w", err) } - commit, err := gitRepo.GetTagCommit(tag.Name) + commit, err := gitRepo.GetTagCommit(ctx, tag.Name) if err != nil { return fmt.Errorf("Commit: %w", err) } diff --git a/services/repository/repository.go b/services/repository/repository.go index 2aef796009..450f06c50c 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -72,6 +72,9 @@ func DeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_mod // PushCreateRepo creates a repository when a new repository is pushed to an appropriate namespace func PushCreateRepo(ctx context.Context, authUser, owner *user_model.User, repoName string) (*repo_model.Repository, error) { + if authUser == nil { + return nil, errors.New("cannot push-create repository anonymously") + } if !authUser.IsAdmin { if owner.IsOrganization() { if ok, err := organization.CanCreateOrgRepo(ctx, owner.ID, authUser.ID); err != nil { @@ -273,6 +276,11 @@ func updateRepository(ctx context.Context, repo *repo_model.Repository, visibili if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil { return err } + + // watchers who lost access must not keep watching the now-private repo + if err = repo_model.ClearRepoWatches(ctx, repo.ID); err != nil { + return err + } } // Create/Remove git-daemon-export-ok for git-daemon... @@ -323,10 +331,10 @@ func CheckCreateRepository(ctx context.Context, doer, owner *user_model.User, na } else if has { return repo_model.ErrRepoAlreadyExist{Uname: owner.Name, Name: name} } - repo := repo_model.StorageRepo(repo_model.RelativePath(owner.Name, name)) + repo := repo_model.CodeRepoByName(owner.Name, name) isExist, err := gitrepo.IsRepositoryExist(ctx, repo) if err != nil { - log.Error("Unable to check if %s exists. Error: %v", repo.RelativePath(), err) + log.Error("Unable to check if repo %s/%s exists, error: %v", owner.Name, name, err) return err } if !overwriteOrAdopt && isExist { diff --git a/services/repository/repository_test.go b/services/repository/repository_test.go index debdfe5e5a..756801c29f 100644 --- a/services/repository/repository_test.go +++ b/services/repository/repository_test.go @@ -90,3 +90,26 @@ func TestMakeRepoPrivateClearsWatches(t *testing.T) { assert.True(t, updatedRepo.IsPrivate) assert.Zero(t, updatedRepo.NumWatches) } + +// TestUpdateRepositoryClearsWatchesOnVisibilityChange ensures the shared updateRepository +// helper (used by the API EditRepo path) also clears watches when a repo goes private. +func TestUpdateRepositoryClearsWatchesOnVisibilityChange(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.False(t, repo.IsPrivate) + + watchers, err := repo_model.GetRepoWatchersIDs(t.Context(), repo.ID) + require.NoError(t, err) + require.NotEmpty(t, watchers) + + repo.IsPrivate = true + require.NoError(t, updateRepository(t.Context(), repo, true)) + + watchers, err = repo_model.GetRepoWatchersIDs(t.Context(), repo.ID) + assert.NoError(t, err) + assert.Empty(t, watchers) + + updatedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) + assert.Zero(t, updatedRepo.NumWatches) +} diff --git a/services/repository/transfer.go b/services/repository/transfer.go index 76051ac4f7..025768e0b3 100644 --- a/services/repository/transfer.go +++ b/services/repository/transfer.go @@ -94,7 +94,7 @@ func isRepositoryModelOrDirExist(ctx context.Context, u *user_model.User, repoNa if err != nil { return false, err } - repo := repo_model.StorageRepo(repo_model.RelativePath(u.Name, repoName)) + repo := repo_model.CodeRepoByName(u.Name, repoName) isExist, err := gitrepo.IsRepositoryExist(ctx, repo) return has || isExist, err } @@ -116,18 +116,19 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName } if repoRenamed { - oldRelativePath, newRelativePath := repo_model.RelativePath(newOwnerName, repo.Name), repo_model.RelativePath(oldOwnerName, repo.Name) - if err := gitrepo.RenameRepository(ctx, repo_model.StorageRepo(oldRelativePath), repo_model.StorageRepo(newRelativePath)); err != nil { - log.Error("Unable to move repository %s/%s directory from %s back to correct place %s: %v", oldOwnerName, repo.Name, - oldRelativePath, newRelativePath, err) + // revert the rename + from := repo_model.CodeRepoByName(newOwnerName, repo.Name) + to := repo_model.CodeRepoByName(oldOwnerName, repo.Name) + if err := gitrepo.RenameRepository(ctx, from, to); err != nil { + log.Error("Unable to revert repository %s/%s to %s/%s: %v", newOwnerName, repo.Name, oldOwnerName, repo.Name, err) } } if wikiRenamed { - oldRelativePath, newRelativePath := repo_model.RelativeWikiPath(newOwnerName, repo.Name), repo_model.RelativeWikiPath(oldOwnerName, repo.Name) - if err := gitrepo.RenameRepository(ctx, repo_model.StorageRepo(oldRelativePath), repo_model.StorageRepo(newRelativePath)); err != nil { - log.Error("Unable to move wiki for repository %s/%s directory from %s back to correct place %s: %v", oldOwnerName, repo.Name, - oldRelativePath, newRelativePath, err) + from := repo_model.WikiRepoByName(newOwnerName, repo.Name) + to := repo_model.WikiRepoByName(oldOwnerName, repo.Name) + if err := gitrepo.RenameRepository(ctx, from, to); err != nil { + log.Error("Unable to revert wiki repository %s/%s to %s/%s: %v", newOwnerName, repo.Name, oldOwnerName, repo.Name, err) } } @@ -302,19 +303,21 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName } // Rename remote repository to new path and delete local copy. - oldRelativePath, newRelativePath := repo_model.RelativePath(oldOwner.Name, repo.Name), repo_model.RelativePath(newOwner.Name, repo.Name) - if err := gitrepo.RenameRepository(ctx, repo_model.StorageRepo(oldRelativePath), repo_model.StorageRepo(newRelativePath)); err != nil { + oldCodeRepo := repo_model.CodeRepoByName(oldOwner.Name, repo.Name) + newCodeRepo := repo_model.CodeRepoByName(newOwner.Name, repo.Name) + if err := gitrepo.RenameRepository(ctx, oldCodeRepo, newCodeRepo); err != nil { return fmt.Errorf("rename repository directory: %w", err) } repoRenamed = true // Rename remote wiki repository to new path and delete local copy. - wikiStorageRepo := repo_model.StorageRepo(repo_model.RelativeWikiPath(oldOwner.Name, repo.Name)) - if isExist, err := gitrepo.IsRepositoryExist(ctx, wikiStorageRepo); err != nil { - log.Error("Unable to check if %s exists. Error: %v", wikiStorageRepo.RelativePath(), err) + oldWikiRepo := repo_model.WikiRepoByName(oldOwner.Name, repo.Name) + if isExist, err := gitrepo.IsRepositoryExist(ctx, oldWikiRepo); err != nil { + log.Error("Unable to check if wiki of repo %s/%s exists. Error: %v", oldOwner.Name, repo.Name, err) return err } else if isExist { - if err := gitrepo.RenameRepository(ctx, wikiStorageRepo, repo_model.StorageRepo(repo_model.RelativeWikiPath(newOwner.Name, repo.Name))); err != nil { + newWikiRepo := repo_model.WikiRepoByName(newOwner.Name, repo.Name) + if err := gitrepo.RenameRepository(ctx, oldWikiRepo, newWikiRepo); err != nil { return fmt.Errorf("rename repository wiki: %w", err) } wikiRenamed = true @@ -373,14 +376,14 @@ func changeRepositoryName(ctx context.Context, repo *repo_model.Repository, newR } } - if err = gitrepo.RenameRepository(ctx, repo, - repo_model.StorageRepo(repo_model.RelativePath(repo.OwnerName, newRepoName))); err != nil { + newCodeRepo := repo_model.CodeRepoByName(repo.OwnerName, newRepoName) + if err = gitrepo.RenameRepository(ctx, repo, newCodeRepo); err != nil { return fmt.Errorf("rename repository directory: %w", err) } if HasWiki(ctx, repo) { - if err = gitrepo.RenameRepository(ctx, repo.WikiStorageRepo(), repo_model.StorageRepo( - repo_model.RelativeWikiPath(repo.OwnerName, newRepoName))); err != nil { + newWikiRepo := repo_model.WikiRepoByName(repo.OwnerName, newRepoName) + if err = gitrepo.RenameRepository(ctx, repo.WikiStorageRepo(), newWikiRepo); err != nil { return fmt.Errorf("rename repository wiki: %w", err) } } diff --git a/services/user/delete.go b/services/user/delete.go index 8679afaf12..e6ff7075c4 100644 --- a/services/user/delete.go +++ b/services/user/delete.go @@ -95,6 +95,11 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error) &user_model.Blocking{BlockerID: u.ID}, &user_model.Blocking{BlockeeID: u.ID}, &actions_model.ActionRunnerToken{OwnerID: u.ID}, + &actions_model.ActionScopedWorkflowSource{OwnerID: u.ID}, + &auth_model.TwoFactor{UID: u.ID}, + &auth_model.WebAuthnCredential{UserID: u.ID}, + &activities_model.Notification{UserID: u.ID}, + &issues_model.IssueWatch{UserID: u.ID}, ); err != nil { return fmt.Errorf("deleteBeans: %w", err) } diff --git a/services/user/update.go b/services/user/update.go index fe1693e1df..441e021774 100644 --- a/services/user/update.go +++ b/services/user/update.go @@ -145,7 +145,8 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er } } - if opts.Visibility.Has() { + // only validate and persist the visibility when it actually changes + if opts.Visibility.Has() && opts.Visibility.Value() != u.Visibility { if !u.IsOrganization() && !setting.Service.AllowedUserVisibilityModesSlice.IsAllowedVisibility(opts.Visibility.Value()) { return fmt.Errorf("visibility mode not allowed: %s", opts.Visibility.Value().String()) } diff --git a/services/user/update_test.go b/services/user/update_test.go index 9d829ad7fd..8ef59a99c0 100644 --- a/services/user/update_test.go +++ b/services/user/update_test.go @@ -10,7 +10,9 @@ import ( user_model "gitea.dev/models/user" password_module "gitea.dev/modules/auth/password" "gitea.dev/modules/optional" + "gitea.dev/modules/setting" "gitea.dev/modules/structs" + "gitea.dev/modules/test" "github.com/stretchr/testify/assert" ) @@ -121,3 +123,33 @@ func TestUpdateAuth(t *testing.T) { Password: optional.Some("aaaa"), }), password_module.ErrMinLength) } + +func TestUpdateUserVisibility(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // user28's current visibility is public, e.g. an account created before public was disallowed + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28}) + assert.Equal(t, structs.VisibleTypePublic, user.Visibility) + + // public is no longer an allowed visibility mode, e.g. ALLOWED_USER_VISIBILITY_MODES = limited, private + defer test.MockVariableValue(&setting.Service.AllowedUserVisibilityModesSlice, setting.AllowedVisibility{false, true, true})() + + // re-submitting the unchanged (now-disallowed) visibility must not fail the whole update + assert.NoError(t, UpdateUser(t.Context(), user, &UpdateOptions{ + FullName: optional.Some("Changed Name"), + Visibility: optional.Some(structs.VisibleTypePublic), + })) + assert.Equal(t, "Changed Name", user.FullName) + assert.Equal(t, structs.VisibleTypePublic, user.Visibility) + + // changing to an allowed visibility still works + assert.NoError(t, UpdateUser(t.Context(), user, &UpdateOptions{ + Visibility: optional.Some(structs.VisibleTypePrivate), + })) + assert.Equal(t, structs.VisibleTypePrivate, user.Visibility) + + // genuinely changing to a disallowed visibility is still rejected + assert.Error(t, UpdateUser(t.Context(), user, &UpdateOptions{ + Visibility: optional.Some(structs.VisibleTypePublic), + })) +} diff --git a/services/user/user_test.go b/services/user/user_test.go index a20694400f..3daf57639f 100644 --- a/services/user/user_test.go +++ b/services/user/user_test.go @@ -9,8 +9,10 @@ import ( "testing" "time" + activities_model "gitea.dev/models/activities" "gitea.dev/models/auth" "gitea.dev/models/db" + issues_model "gitea.dev/models/issues" "gitea.dev/models/organization" repo_model "gitea.dev/models/repo" "gitea.dev/models/unittest" @@ -61,6 +63,32 @@ func TestDeleteUser(t *testing.T) { org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) assert.Error(t, DeleteUser(t.Context(), org, false)) + + t.Run("CleanupOrphanedTables", func(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // assert they exist before deletion + unittest.AssertExistsAndLoadBean(t, &auth.TwoFactor{UID: 24}) + unittest.AssertExistsAndLoadBean(t, &auth.WebAuthnCredential{UserID: 32}) + unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{UserID: 2}) + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueWatch{UserID: 2}) + + // delete users + user24 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 24}) + assert.NoError(t, DeleteUser(t.Context(), user24, true)) + + user32 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 32}) + assert.NoError(t, DeleteUser(t.Context(), user32, true)) + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + assert.NoError(t, DeleteUser(t.Context(), user2, true)) + + // assert they do not exist after deletion + unittest.AssertNotExistsBean(t, &auth.TwoFactor{UID: 24}) + unittest.AssertNotExistsBean(t, &auth.WebAuthnCredential{UserID: 32}) + unittest.AssertNotExistsBean(t, &activities_model.Notification{UserID: 2}) + unittest.AssertNotExistsBean(t, &issues_model.IssueWatch{UserID: 2}) + }) } func TestDeleteUserUnlinkedAttachments(t *testing.T) { diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index 2858275cd5..fc674f7ca6 100644 --- a/services/webhook/deliver.go +++ b/services/webhook/deliver.go @@ -308,20 +308,14 @@ func webhookProxy(allowList *hostmatcher.HostMatchList) func(req *http.Request) // Init starts the hooks delivery thread func Init() error { timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second + allowedHostMatcher := hostmatcher.ParseHostMatchList("security.ALLOWED_HOST_LIST", setting.Webhook.AllowedHostList) - allowedHostListValue := setting.Webhook.AllowedHostList - if allowedHostListValue == "" { - allowedHostListValue = hostmatcher.MatchBuiltinExternal - } - allowedHostMatcher := hostmatcher.ParseHostMatchList("webhook.ALLOWED_HOST_LIST", allowedHostListValue) - + // NewHTTPTransport enforces the allow-list on direct connections; when webhookProxy routes a request + // through a configured proxy, restricting the proxied target is the proxy server's responsibility. webhookHTTPClient = &http.Client{ Timeout: timeout, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify}, - Proxy: webhookProxy(allowedHostMatcher), - DialContext: hostmatcher.NewDialContext("webhook", allowedHostMatcher, nil, setting.Webhook.ProxyURLFixed), - }, + Transport: hostmatcher.NewHTTPTransport("webhook", allowedHostMatcher, nil, webhookProxy(allowedHostMatcher), setting.Webhook.ProxyURLFixed, + &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify}), } hookQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "webhook_sender", handler) diff --git a/services/webhook/general.go b/services/webhook/general.go index 021f979b8e..327e36c120 100644 --- a/services/webhook/general.go +++ b/services/webhook/general.go @@ -404,20 +404,17 @@ func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) { config["color"] = s.Color } - authorizationHeader, err := w.HeaderAuthorization() - if err != nil { - return nil, err - } - return &api.Hook{ - ID: w.ID, - Name: w.Name, - Type: w.Type, - URL: fmt.Sprintf("%s/settings/hooks/%d", repoLink, w.ID), - Active: w.IsActive, - Config: config, - Events: w.EventsArray(), - AuthorizationHeader: authorizationHeader, + ID: w.ID, + Name: w.Name, + Type: w.Type, + URL: fmt.Sprintf("%s/settings/hooks/%d", repoLink, w.ID), + Active: w.IsActive, + Config: config, + Events: w.EventsArray(), + // the stored authorization header is a secret and must never be returned by the API, + // consistent with the webhook secret which is also omitted from the response + AuthorizationHeader: "", Updated: w.UpdatedUnix.AsTime(), Created: w.CreatedUnix.AsTime(), BranchFilter: w.BranchFilter, diff --git a/services/webhook/main_test.go b/services/webhook/main_test.go index bca21baaa6..9bed4df130 100644 --- a/services/webhook/main_test.go +++ b/services/webhook/main_test.go @@ -15,10 +15,11 @@ import ( ) func TestMain(m *testing.M) { - // for tests, allow only loopback IPs - setting.Webhook.AllowedHostList = hostmatcher.MatchBuiltinLoopback unittest.MainTest(m, &unittest.TestOptions{ SetUp: func() error { + // for tests, allow only loopback IPs. This must run after the test config is loaded (which + // resets the shared Security.AllowedHostList) and before Init() builds the delivery client. + setting.Security.AllowedHostList = hostmatcher.MatchBuiltinLoopback setting.LoadQueueSettings() return Init() }, diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index 7024036da1..c7881444db 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -187,6 +187,18 @@ func (m msteamsConvertor) IssueComment(p *api.IssueCommentPayload) (MSTeamsPaylo func (m msteamsConvertor) PullRequest(p *api.PullRequestPayload) (MSTeamsPayload, error) { title, _, extraMarkdown, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false) + facts := []*MSTeamsFact{ + {"Pull request:", fmt.Sprintf("[#%d](%s)", p.PullRequest.Index, p.PullRequest.HTMLURL)}, + } + + if (p.Action == api.HookIssueReviewRequested || p.Action == api.HookIssueReviewRequestRemoved) && p.RequestedReviewer != nil { + reviewerName := p.RequestedReviewer.UserName + if p.RequestedReviewer.FullName != "" { + reviewerName += " (" + p.RequestedReviewer.FullName + ")" + } + facts = append(facts, &MSTeamsFact{"Requested Reviewer:", reviewerName}) + } + return createMSTeamsPayload( p.Repository, p.Sender, @@ -194,7 +206,7 @@ func (m msteamsConvertor) PullRequest(p *api.PullRequestPayload) (MSTeamsPayload extraMarkdown, p.PullRequest.HTMLURL, color, - &MSTeamsFact{"Pull request #:", strconv.FormatInt(p.PullRequest.ID, 10)}, + facts..., ), nil } @@ -231,7 +243,7 @@ func (m msteamsConvertor) Review(p *api.PullRequestPayload, event webhook_module text, p.PullRequest.HTMLURL, color, - &MSTeamsFact{"Pull request #:", strconv.FormatInt(p.PullRequest.ID, 10)}, + &MSTeamsFact{"Pull request:", fmt.Sprintf("[#%d](%s)", p.PullRequest.Index, p.PullRequest.HTMLURL)}, ), nil } @@ -271,7 +283,6 @@ func (m msteamsConvertor) Wiki(p *api.WikiPayload) (MSTeamsPayload, error) { "", p.Repository.HTMLURL+"/wiki/"+url.PathEscape(p.Page), color, - &MSTeamsFact{"Repository:", p.Repository.FullName}, ), nil } @@ -346,16 +357,18 @@ func (msteamsConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MSTeamsPayload, ), nil } -func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload { - facts := make([]MSTeamsFact, 0, 2) +func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, extraFacts ...*MSTeamsFact) MSTeamsPayload { + facts := make([]MSTeamsFact, 0, len(extraFacts)+1) if r != nil { facts = append(facts, MSTeamsFact{ Name: "Repository:", - Value: r.FullName, + Value: fmt.Sprintf("[%s](%s)", r.FullName, r.HTMLURL), }) } - if fact != nil { - facts = append(facts, *fact) + for _, f := range extraFacts { + if f != nil { + facts = append(facts, *f) + } } return MSTeamsPayload{ diff --git a/services/webhook/msteams_test.go b/services/webhook/msteams_test.go index cf7f9fbb9e..94112356f7 100644 --- a/services/webhook/msteams_test.go +++ b/services/webhook/msteams_test.go @@ -4,6 +4,7 @@ package webhook import ( + "fmt" "testing" webhook_model "gitea.dev/models/webhook" @@ -31,7 +32,7 @@ func TestMSTeamsPayload(t *testing.T) { assert.Len(t, pl.Sections[0].Facts, 2) for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { - assert.Equal(t, p.Repo.FullName, fact.Value) + assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repo.FullName, p.Repo.HTMLURL), fact.Value) } else if fact.Name == "branch:" { assert.Equal(t, "test", fact.Value) } else { @@ -57,7 +58,7 @@ func TestMSTeamsPayload(t *testing.T) { assert.Len(t, pl.Sections[0].Facts, 2) for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { - assert.Equal(t, p.Repo.FullName, fact.Value) + assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repo.FullName, p.Repo.HTMLURL), fact.Value) } else if fact.Name == "branch:" { assert.Equal(t, "test", fact.Value) } else { @@ -83,7 +84,7 @@ func TestMSTeamsPayload(t *testing.T) { assert.Len(t, pl.Sections[0].Facts, 2) for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { - assert.Equal(t, p.Repo.FullName, fact.Value) + assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repo.FullName, p.Repo.HTMLURL), fact.Value) } else if fact.Name == "Forkee:" { assert.Equal(t, p.Forkee.FullName, fact.Value) } else { @@ -109,7 +110,7 @@ func TestMSTeamsPayload(t *testing.T) { assert.Len(t, pl.Sections[0].Facts, 2) for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { - assert.Equal(t, p.Repo.FullName, fact.Value) + assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repo.FullName, p.Repo.HTMLURL), fact.Value) } else if fact.Name == "Commit count:" { assert.Equal(t, "2", fact.Value) } else { @@ -136,7 +137,7 @@ func TestMSTeamsPayload(t *testing.T) { assert.Len(t, pl.Sections[0].Facts, 2) for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { - assert.Equal(t, p.Repository.FullName, fact.Value) + assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value) } else if fact.Name == "Issue #:" { assert.Equal(t, "2", fact.Value) } else { @@ -159,7 +160,7 @@ func TestMSTeamsPayload(t *testing.T) { assert.Len(t, pl.Sections[0].Facts, 2) for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { - assert.Equal(t, p.Repository.FullName, fact.Value) + assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value) } else if fact.Name == "Issue #:" { assert.Equal(t, "2", fact.Value) } else { @@ -185,7 +186,7 @@ func TestMSTeamsPayload(t *testing.T) { assert.Len(t, pl.Sections[0].Facts, 2) for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { - assert.Equal(t, p.Repository.FullName, fact.Value) + assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value) } else if fact.Name == "Issue #:" { assert.Equal(t, "2", fact.Value) } else { @@ -211,9 +212,9 @@ func TestMSTeamsPayload(t *testing.T) { assert.Len(t, pl.Sections[0].Facts, 2) for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { - assert.Equal(t, p.Repository.FullName, fact.Value) - } else if fact.Name == "Pull request #:" { - assert.Equal(t, "12", fact.Value) + assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value) + } else if fact.Name == "Pull request:" { + assert.Equal(t, fmt.Sprintf("[#%d](%s)", p.PullRequest.Index, p.PullRequest.HTMLURL), fact.Value) } else { t.Fail() } @@ -223,6 +224,34 @@ func TestMSTeamsPayload(t *testing.T) { assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI) }) + t.Run("PullRequestReviewRequest", func(t *testing.T) { + p := pullRequestTestPayload() + p.Action = api.HookIssueReviewRequested + p.RequestedReviewer = &api.User{ + UserName: "reviewer1", + FullName: "Reviewer One", + } + + pl, err := mc.PullRequest(p) + require.NoError(t, err) + + assert.Len(t, pl.Sections[0].Facts, 3) + var hasReviewer bool + for _, fact := range pl.Sections[0].Facts { + if fact.Name == "Repository:" { + assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value) + } else if fact.Name == "Pull request:" { + assert.Equal(t, fmt.Sprintf("[#%d](%s)", p.PullRequest.Index, p.PullRequest.HTMLURL), fact.Value) + } else if fact.Name == "Requested Reviewer:" { + assert.Equal(t, "reviewer1 (Reviewer One)", fact.Value) + hasReviewer = true + } else { + t.Fail() + } + } + assert.True(t, hasReviewer) + }) + t.Run("PullRequestComment", func(t *testing.T) { p := pullRequestCommentTestPayload() @@ -237,7 +266,7 @@ func TestMSTeamsPayload(t *testing.T) { assert.Len(t, pl.Sections[0].Facts, 2) for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { - assert.Equal(t, p.Repository.FullName, fact.Value) + assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value) } else if fact.Name == "Issue #:" { assert.Equal(t, "12", fact.Value) } else { @@ -264,9 +293,9 @@ func TestMSTeamsPayload(t *testing.T) { assert.Len(t, pl.Sections[0].Facts, 2) for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { - assert.Equal(t, p.Repository.FullName, fact.Value) - } else if fact.Name == "Pull request #:" { - assert.Equal(t, "12", fact.Value) + assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value) + } else if fact.Name == "Pull request:" { + assert.Equal(t, fmt.Sprintf("[#%d](%s)", p.PullRequest.Index, p.PullRequest.HTMLURL), fact.Value) } else { t.Fail() } @@ -290,7 +319,7 @@ func TestMSTeamsPayload(t *testing.T) { assert.Len(t, pl.Sections[0].Facts, 1) for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { - assert.Equal(t, p.Repository.FullName, fact.Value) + assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value) } else { t.Fail() } @@ -336,17 +365,14 @@ func TestMSTeamsPayload(t *testing.T) { assert.Len(t, pl.Sections, 1) assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) assert.Empty(t, pl.Sections[0].Text) - assert.Len(t, pl.Sections[0].Facts, 2) + assert.Len(t, pl.Sections[0].Facts, 1) for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { - assert.Equal(t, p.Repository.FullName, fact.Value) + assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value) } else { t.Fail() } } - assert.Len(t, pl.PotentialAction, 1) - assert.Len(t, pl.PotentialAction[0].Targets, 1) - assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI) p.Action = api.HookWikiEdited pl, err = mc.Wiki(p) @@ -357,10 +383,10 @@ func TestMSTeamsPayload(t *testing.T) { assert.Len(t, pl.Sections, 1) assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) assert.Empty(t, pl.Sections[0].Text) - assert.Len(t, pl.Sections[0].Facts, 2) + assert.Len(t, pl.Sections[0].Facts, 1) for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { - assert.Equal(t, p.Repository.FullName, fact.Value) + assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value) } else { t.Fail() } @@ -378,10 +404,10 @@ func TestMSTeamsPayload(t *testing.T) { assert.Len(t, pl.Sections, 1) assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) assert.Empty(t, pl.Sections[0].Text) - assert.Len(t, pl.Sections[0].Facts, 2) + assert.Len(t, pl.Sections[0].Facts, 1) for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { - assert.Equal(t, p.Repository.FullName, fact.Value) + assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value) } else { t.Fail() } @@ -405,7 +431,7 @@ func TestMSTeamsPayload(t *testing.T) { assert.Len(t, pl.Sections[0].Facts, 2) for _, fact := range pl.Sections[0].Facts { if fact.Name == "Repository:" { - assert.Equal(t, p.Repository.FullName, fact.Value) + assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value) } else if fact.Name == "Tag:" { assert.Equal(t, "v1.0", fact.Value) } else { diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index b9aaf52f85..2586d09798 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -17,7 +17,6 @@ import ( repo_model "gitea.dev/models/repo" user_model "gitea.dev/models/user" "gitea.dev/modules/git" - "gitea.dev/modules/gitrepo" "gitea.dev/modules/httplib" "gitea.dev/modules/log" "gitea.dev/modules/repository" @@ -1029,19 +1028,14 @@ func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_ status := convert.ToWorkflowRunAction(run.Status) - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + // Resolve the workflow definition from its source repo. + convertedWorkflow, err := convert.ResolveActionWorkflowForRun(ctx, repo, run) if err != nil { - log.Error("OpenRepository: %v", err) - return - } - defer gitRepo.Close() - - convertedWorkflow, err := convert.GetActionWorkflowByRef(ctx, gitRepo, repo, run.WorkflowID, git.RefName(run.Ref)) - if err != nil && errors.Is(err, util.ErrNotExist) { - convertedWorkflow, err = convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) - } - if err != nil { - log.Error("GetActionWorkflow: %v", err) + if errors.Is(err, util.ErrNotExist) { + log.Debug("WorkflowRunStatusUpdate: workflow %q for run %d not found: %v", run.WorkflowID, run.ID, err) + return + } + log.Error("ResolveActionWorkflowForRun: %v", err) return } diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index 99a0b417b8..cae9a883c7 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -8,6 +8,7 @@ import ( "fmt" "html" "net/http" + "net/url" "strings" webhook_model "gitea.dev/models/webhook" @@ -23,9 +24,12 @@ import ( type ( // TelegramPayload represents TelegramPayload struct { - Message string `json:"text"` - ParseMode string `json:"parse_mode"` - DisableWebPreview bool `json:"disable_web_page_preview"` + RichMessage InputRichMessage `json:"rich_message"` + } + + // InputRichMessage represents input rich message + InputRichMessage struct { + HTML string `json:"html"` } // TelegramMeta contains the telegram metadata @@ -195,13 +199,21 @@ func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload func createTelegramPayloadHTML(msgHTML string) TelegramPayload { // https://core.telegram.org/bots/api#formatting-options return TelegramPayload{ - Message: strings.TrimSpace(string(markup.Sanitize(msgHTML))), - ParseMode: "HTML", - DisableWebPreview: true, + RichMessage: InputRichMessage{ + HTML: strings.TrimSpace(string(markup.Sanitize(msgHTML))), + }, } } func newTelegramRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { + u, err := url.Parse(w.URL) + if err != nil { + return nil, nil, err + } + if urlPrefix, ok := strings.CutSuffix(u.Path, "/sendMessage"); ok { + u.Path = urlPrefix + "/sendRichMessage" + w.URL = u.String() + } var pc payloadConvertor[TelegramPayload] = telegramConvertor{} return newJSONRequest(pc, w, t, true) } diff --git a/services/webhook/telegram_test.go b/services/webhook/telegram_test.go index 7c8209577e..3e615af814 100644 --- a/services/webhook/telegram_test.go +++ b/services/webhook/telegram_test.go @@ -21,9 +21,9 @@ func TestTelegramPayload(t *testing.T) { t.Run("Correct webhook params", func(t *testing.T) { p := createTelegramPayloadHTML(`testMsg `) assert.Equal(t, TelegramPayload{ - Message: `testMsg`, - ParseMode: "HTML", - DisableWebPreview: true, + RichMessage: InputRichMessage{ + HTML: `testMsg`, + }, }, p) }) @@ -33,7 +33,7 @@ func TestTelegramPayload(t *testing.T) { pl, err := tc.Create(p) require.NoError(t, err) - assert.Equal(t, `[test/repo] branch test created`, pl.Message) + assert.Equal(t, `[test/repo] branch test created`, pl.RichMessage.HTML) }) t.Run("Delete", func(t *testing.T) { @@ -42,7 +42,7 @@ func TestTelegramPayload(t *testing.T) { pl, err := tc.Delete(p) require.NoError(t, err) - assert.Equal(t, `[test/repo] branch test deleted`, pl.Message) + assert.Equal(t, `[test/repo] branch test deleted`, pl.RichMessage.HTML) }) t.Run("Fork", func(t *testing.T) { @@ -51,7 +51,7 @@ func TestTelegramPayload(t *testing.T) { pl, err := tc.Fork(p) require.NoError(t, err) - assert.Equal(t, `test/repo2 is forked to test/repo`, pl.Message) + assert.Equal(t, `test/repo2 is forked to test/repo`, pl.RichMessage.HTML) }) t.Run("Push", func(t *testing.T) { @@ -62,7 +62,7 @@ func TestTelegramPayload(t *testing.T) { assert.Equal(t, `[test/repo:test] 2 new commits [2020558] commit message - user1 -[2020558] commit message - user1`, pl.Message) +[2020558] commit message - user1`, pl.RichMessage.HTML) }) t.Run("Issue", func(t *testing.T) { @@ -74,13 +74,13 @@ func TestTelegramPayload(t *testing.T) { assert.Equal(t, `[test/repo] Issue opened: #2 crash by user1 -issue body`, pl.Message) +issue body`, pl.RichMessage.HTML) p.Action = api.HookIssueClosed pl, err = tc.Issue(p) require.NoError(t, err) - assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.Message) + assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.RichMessage.HTML) }) t.Run("IssueComment", func(t *testing.T) { @@ -90,7 +90,7 @@ issue body`, pl.Message) require.NoError(t, err) assert.Equal(t, `[test/repo] New comment on issue #2 crash by user1 -more info needed`, pl.Message) +more info needed`, pl.RichMessage.HTML) }) t.Run("PullRequest", func(t *testing.T) { @@ -100,7 +100,7 @@ more info needed`, pl.Message) require.NoError(t, err) assert.Equal(t, `[test/repo] Pull request opened: #12 Fix bug by user1 -fixes bug #2`, pl.Message) +fixes bug #2`, pl.RichMessage.HTML) }) t.Run("PullRequestComment", func(t *testing.T) { @@ -110,7 +110,7 @@ fixes bug #2`, pl.Message) require.NoError(t, err) assert.Equal(t, `[test/repo] New comment on pull request #12 Fix bug by user1 -changes requested`, pl.Message) +changes requested`, pl.RichMessage.HTML) }) t.Run("Review", func(t *testing.T) { @@ -121,7 +121,7 @@ changes requested`, pl.Message) require.NoError(t, err) assert.Equal(t, `[test/repo] Pull request review approved: #12 Fix bug -good job`, pl.Message) +good job`, pl.RichMessage.HTML) }) t.Run("Repository", func(t *testing.T) { @@ -130,7 +130,7 @@ good job`, pl.Message) pl, err := tc.Repository(p) require.NoError(t, err) - assert.Equal(t, `[test/repo] Repository created`, pl.Message) + assert.Equal(t, `[test/repo] Repository created`, pl.RichMessage.HTML) }) t.Run("Package", func(t *testing.T) { @@ -139,7 +139,7 @@ good job`, pl.Message) pl, err := tc.Package(p) require.NoError(t, err) - assert.Equal(t, `Package created: GiteaContainer:latest by user1`, pl.Message) + assert.Equal(t, `Package created: GiteaContainer:latest by user1`, pl.RichMessage.HTML) }) t.Run("Wiki", func(t *testing.T) { @@ -149,19 +149,19 @@ good job`, pl.Message) pl, err := tc.Wiki(p) require.NoError(t, err) - assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.Message) + assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.RichMessage.HTML) p.Action = api.HookWikiEdited pl, err = tc.Wiki(p) require.NoError(t, err) - assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.Message) + assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.RichMessage.HTML) p.Action = api.HookWikiDeleted pl, err = tc.Wiki(p) require.NoError(t, err) - assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.Message) + assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.RichMessage.HTML) }) t.Run("Release", func(t *testing.T) { @@ -170,7 +170,7 @@ good job`, pl.Message) pl, err := tc.Release(p) require.NoError(t, err) - assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.Message) + assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.RichMessage.HTML) }) } @@ -183,7 +183,7 @@ func TestTelegramJSONPayload(t *testing.T) { RepoID: 3, IsActive: true, Type: webhook_module.TELEGRAM, - URL: "https://telegram.example.com/", + URL: "https://telegram.example.com/sendMessage", Meta: ``, HTTPMethod: "POST", } @@ -200,7 +200,7 @@ func TestTelegramJSONPayload(t *testing.T) { require.NoError(t, err) assert.Equal(t, "POST", req.Method) - assert.Equal(t, "https://telegram.example.com/", req.URL.String()) + assert.Equal(t, "https://telegram.example.com/sendRichMessage", req.URL.String()) assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) assert.Equal(t, "application/json", req.Header.Get("Content-Type")) var body TelegramPayload @@ -208,5 +208,5 @@ func TestTelegramJSONPayload(t *testing.T) { assert.NoError(t, err) assert.Equal(t, `[test/repo:test] 2 new commits [2020558] commit message - user1 -[2020558] commit message - user1`, body.Message) +[2020558] commit message - user1`, body.RichMessage.HTML) } diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index 5e8107772a..0a53f289aa 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -129,6 +129,33 @@ func checkBranchFilter(branchFilter string, ref git.RefName) bool { return g.Match(ref.String()) } +// PrepareTestWebhook always creates and enqueues a hook task for manual testing. +// Unlike PrepareWebhook, it ignores event subscriptions and branch filters so the +// Test Push Event control can verify delivery even when those gates would suppress +// a real event. +func PrepareTestWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook_module.HookEventType, p api.Payloader) error { + if setting.DisableWebhooks { + return nil + } + + payload, err := p.JSONPayload() + if err != nil { + return fmt.Errorf("JSONPayload for %s: %w", event, err) + } + + task, err := webhook_model.CreateHookTask(ctx, &webhook_model.HookTask{ + HookID: w.ID, + PayloadContent: string(payload), + EventType: event, + PayloadVersion: 2, + }) + if err != nil { + return fmt.Errorf("CreateHookTask for %s: %w", event, err) + } + + return enqueueHookTask(task.ID) +} + // PrepareWebhook creates a hook task and enqueues it for processing. // The payload is saved as-is. The adjustments depending on the webhook type happen // right before delivery, in the [Deliver] method. diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go index f20510c9a6..6ecf24337c 100644 --- a/services/webhook/webhook_test.go +++ b/services/webhook/webhook_test.go @@ -30,6 +30,7 @@ func TestWebhookService(t *testing.T) { t.Run("PrepareBranchFilterNoMatch", testWebhookPrepareBranchFilterNoMatch) t.Run("WebhookUserMail", testWebhookUserMail) t.Run("CheckBranchFilter", testWebhookCheckBranchFilter) + t.Run("PrepareTestWebhookIgnoresGates", testPrepareTestWebhookIgnoresGates) } func testWebhookGetSlackHook(t *testing.T) { @@ -132,3 +133,37 @@ func testWebhookCheckBranchFilter(t *testing.T) { assert.Equal(t, v.match, checkBranchFilter(v.filter, v.ref), "filter: %q ref: %q", v.filter, v.ref) } } + +func testPrepareTestWebhookIgnoresGates(t *testing.T) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + hook := &webhook_model.Webhook{ + RepoID: repo.ID, + URL: "http://localhost/gitea-webhook-test-prepare_test_webhook", + ContentType: webhook_model.ContentTypeJSON, + IsActive: true, + HookEvent: &webhook_module.HookEvent{ + ChooseEvents: true, + BranchFilter: "dev", + HookEvents: webhook_module.HookEvents{ + webhook_module.HookEventWorkflowRun: true, + }, + }, + } + require.NoError(t, hook.UpdateEvent()) + require.NoError(t, db.Insert(t.Context(), hook)) + + payload := &api.PushPayload{ + Ref: "refs/heads/master", + Commits: []*api.PayloadCommit{{}}, + } + hookTask := &webhook_model.HookTask{HookID: hook.ID, EventType: webhook_module.HookEventPush} + + // Real deliveries stay gated: no push event + branch filter mismatch => nothing queued. + unittest.AssertNotExistsBean(t, hookTask) + require.NoError(t, PrepareWebhook(t.Context(), hook, webhook_module.HookEventPush, payload)) + unittest.AssertNotExistsBean(t, hookTask) + + // Manual test delivery always queues so the endpoint can be verified. + require.NoError(t, PrepareTestWebhook(t.Context(), hook, webhook_module.HookEventPush, payload)) + unittest.AssertExistsAndLoadBean(t, hookTask) +} diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index d1006abf7e..0b20617ab8 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -52,12 +52,12 @@ func InitWiki(ctx context.Context, repo *repo_model.Repository) error { // prepareGitPath try to find a suitable file path with file name by the given raw wiki name. // return: existence, prepared file path with name, error -func prepareGitPath(gitRepo *git.Repository, defaultWikiBranch string, wikiPath WebPath) (bool, string, error) { +func prepareGitPath(ctx context.Context, gitRepo *git.Repository, defaultWikiBranch string, wikiPath WebPath) (bool, string, error) { unescaped := string(wikiPath) + ".md" gitPath := WebPathToGitPath(wikiPath) // Look for both files - filesInIndex, err := gitRepo.LsTree(defaultWikiBranch, unescaped, gitPath) + filesInIndex, err := gitRepo.LsTree(ctx, defaultWikiBranch, unescaped, gitPath) if err != nil { if gitcmd.IsStderr(err, gitcmd.StderrNotValidObjectName) { return false, gitPath, nil // branch doesn't exist @@ -123,7 +123,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err) } - gitRepo, err := git.OpenRepository(ctx, basePath) + gitRepo, err := git.OpenRepositoryLocal(basePath) if err != nil { log.Error("Unable to open temporary repository: %s (%v)", basePath, err) return fmt.Errorf("failed to open new temporary repository in: %s %w", basePath, err) @@ -131,13 +131,13 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model defer gitRepo.Close() if hasDefaultBranch { - if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil { + if err := gitRepo.ReadTreeToIndex(ctx, "HEAD"); err != nil { log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err) return fmt.Errorf("unable to read HEAD tree to index in: %s %w", basePath, err) } } - isWikiExist, newWikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, newWikiName) + isWikiExist, newWikiPath, err := prepareGitPath(ctx, gitRepo, repo.DefaultWikiBranch, newWikiName) if err != nil { return err } @@ -153,14 +153,14 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model isOldWikiExist := true oldWikiPath := newWikiPath if oldWikiName != newWikiName { - isOldWikiExist, oldWikiPath, err = prepareGitPath(gitRepo, repo.DefaultWikiBranch, oldWikiName) + isOldWikiExist, oldWikiPath, err = prepareGitPath(ctx, gitRepo, repo.DefaultWikiBranch, oldWikiName) if err != nil { return err } } if isOldWikiExist { - err := gitRepo.RemoveFilesFromIndex(oldWikiPath) + err := gitRepo.RemoveFilesFromIndex(ctx, oldWikiPath) if err != nil { log.Error("RemoveFilesFromIndex failed: %v", err) return err @@ -170,18 +170,18 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here - objectHash, err := gitRepo.HashObjectBytes([]byte(content)) + objectHash, err := gitRepo.HashObjectBytes(ctx, []byte(content)) if err != nil { log.Error("HashObject failed: %v", err) return err } - if err := gitRepo.AddObjectToIndex("100644", objectHash, newWikiPath); err != nil { + if err := gitRepo.AddObjectToIndex(ctx, "100644", objectHash, newWikiPath); err != nil { log.Error("AddObjectToIndex failed: %v", err) return err } - tree, err := gitRepo.WriteTree() + tree, err := gitRepo.WriteTree(ctx) if err != nil { log.Error("WriteTree failed: %v", err) return err @@ -212,7 +212,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model commitTreeOpts.Parents = []string{"HEAD"} } - commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts) + commitHash, err := gitRepo.CommitTree(ctx, doer.NewGitSig(), committer, tree, commitTreeOpts) if err != nil { log.Error("CommitTree failed: %v", err) return err @@ -282,24 +282,24 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err) } - gitRepo, err := git.OpenRepository(ctx, basePath) + gitRepo, err := git.OpenRepositoryLocal(basePath) if err != nil { log.Error("Unable to open temporary repository: %s (%v)", basePath, err) return fmt.Errorf("failed to open new temporary repository in: %s %w", basePath, err) } defer gitRepo.Close() - if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil { + if err := gitRepo.ReadTreeToIndex(ctx, "HEAD"); err != nil { log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err) return fmt.Errorf("unable to read HEAD tree to index in: %s %w", basePath, err) } - found, wikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, wikiName) + found, wikiPath, err := prepareGitPath(ctx, gitRepo, repo.DefaultWikiBranch, wikiName) if err != nil { return err } if found { - err := gitRepo.RemoveFilesFromIndex(wikiPath) + err := gitRepo.RemoveFilesFromIndex(ctx, wikiPath) if err != nil { return err } @@ -309,7 +309,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here - tree, err := gitRepo.WriteTree() + tree, err := gitRepo.WriteTree(ctx) if err != nil { return err } @@ -337,12 +337,12 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model commitTreeOpts.NoGPGSign = true } - commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts) + commitHash, err := gitRepo.CommitTree(ctx, doer.NewGitSig(), committer, tree, commitTreeOpts) if err != nil { return err } - if err := gitrepo.PushFromLocal(gitRepo.Ctx, basePath, repo.WikiStorageRepo(), git.PushOptions{ + if err := gitrepo.PushFromLocal(ctx, basePath, repo.WikiStorageRepo(), git.PushOptions{ Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch), Env: repo_module.FullPushingEnvironment( doer, diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go index 9b1160d7d9..36889c7318 100644 --- a/services/wiki/wiki_test.go +++ b/services/wiki/wiki_test.go @@ -167,14 +167,14 @@ func TestRepository_AddWikiPage(t *testing.T) { webPath := UserTitleToWebPath("", userTitle) assert.NoError(t, AddWikiPage(t.Context(), doer, repo, webPath, wikiContent, commitMsg)) // Now need to show that the page has been added: - gitRepo, err := gitrepo.OpenRepository(t.Context(), repo.WikiStorageRepo()) + gitRepo, err := gitrepo.OpenRepository(repo.WikiStorageRepo()) require.NoError(t, err) defer gitRepo.Close() - masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch) + masterTree, err := gitRepo.GetTree(t.Context(), repo.DefaultWikiBranch) assert.NoError(t, err) gitPath := WebPathToGitPath(webPath) - entry, err := masterTree.GetTreeEntryByPath(gitPath) + entry, err := masterTree.GetTreeEntryByPath(t.Context(), gitRepo, gitPath) assert.NoError(t, err) assert.Equal(t, gitPath, entry.Name(), "%s not added correctly", userTitle) }) @@ -214,17 +214,17 @@ func TestRepository_EditWikiPage(t *testing.T) { assert.NoError(t, EditWikiPage(t.Context(), doer, repo, "Home", webPath, newWikiContent, commitMsg)) // Now need to show that the page has been added: - gitRepo, err := gitrepo.OpenRepository(t.Context(), repo.WikiStorageRepo()) + gitRepo, err := gitrepo.OpenRepository(repo.WikiStorageRepo()) assert.NoError(t, err) - masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch) + masterTree, err := gitRepo.GetTree(t.Context(), repo.DefaultWikiBranch) assert.NoError(t, err) gitPath := WebPathToGitPath(webPath) - entry, err := masterTree.GetTreeEntryByPath(gitPath) + entry, err := masterTree.GetTreeEntryByPath(t.Context(), gitRepo, gitPath) assert.NoError(t, err) assert.Equal(t, gitPath, entry.Name(), "%s not edited correctly", newWikiName) if newWikiName != "Home" { - _, err := masterTree.GetTreeEntryByPath("Home.md") + _, err := masterTree.GetTreeEntryByPath(t.Context(), gitRepo, "Home.md") assert.Error(t, err) } gitRepo.Close() @@ -238,21 +238,21 @@ func TestRepository_DeleteWikiPage(t *testing.T) { assert.NoError(t, DeleteWikiPage(t.Context(), doer, repo, "Home")) // Now need to show that the page has been added: - gitRepo, err := gitrepo.OpenRepository(t.Context(), repo.WikiStorageRepo()) + gitRepo, err := gitrepo.OpenRepository(repo.WikiStorageRepo()) require.NoError(t, err) defer gitRepo.Close() - masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch) + masterTree, err := gitRepo.GetTree(t.Context(), repo.DefaultWikiBranch) assert.NoError(t, err) gitPath := WebPathToGitPath("Home") - _, err = masterTree.GetTreeEntryByPath(gitPath) + _, err = masterTree.GetTreeEntryByPath(t.Context(), gitRepo, gitPath) assert.Error(t, err) } func TestPrepareWikiFileName(t *testing.T) { unittest.PrepareTestEnv(t) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - gitRepo, err := gitrepo.OpenRepository(t.Context(), repo.WikiStorageRepo()) + gitRepo, err := gitrepo.OpenRepository(repo.WikiStorageRepo()) require.NoError(t, err) defer gitRepo.Close() @@ -279,7 +279,7 @@ func TestPrepareWikiFileName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { webPath := UserTitleToWebPath("", tt.arg) - existence, newWikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, webPath) + existence, newWikiPath, err := prepareGitPath(t.Context(), gitRepo, repo.DefaultWikiBranch, webPath) if (err != nil) != tt.wantErr { assert.NoError(t, err) return @@ -305,12 +305,12 @@ func TestPrepareWikiFileName_FirstPage(t *testing.T) { err := git.InitRepository(t.Context(), tmpDir, true, git.Sha1ObjectFormat.Name()) assert.NoError(t, err) - gitRepo, err := git.OpenRepository(t.Context(), tmpDir) + gitRepo, err := git.OpenRepositoryLocal(tmpDir) require.NoError(t, err) defer gitRepo.Close() - existence, newWikiPath, err := prepareGitPath(gitRepo, "master", "Home") + existence, newWikiPath, err := prepareGitPath(t.Context(), gitRepo, "master", "Home") assert.False(t, existence) assert.NoError(t, err) assert.Equal(t, "Home.md", newWikiPath) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 2fea108de1..084c13cc8e 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -12,9 +12,6 @@ base: core24 adopt-info: gitea platforms: - armhf: - build-on: [armhf] - build-for: [armhf] amd64: build-on: [amd64] build-for: [amd64] diff --git a/tailwind.config.ts b/tailwind.config.ts index 199433d0cf..f597121743 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -90,7 +90,7 @@ export default { '8xl': '96px', '9xl': '128px', ...Object.fromEntries(Array.from({length: 100}, (_, i) => { - return [`${i}`, `${i === 0 ? '0' : `${i}px`}`]; + return [String(i), i === 0 ? '0' : `${i}px`]; })), }, extend: { diff --git a/templates/admin/actions.tmpl b/templates/admin/actions.tmpl index 1bddb674c0..6be9bdf73b 100644 --- a/templates/admin/actions.tmpl +++ b/templates/admin/actions.tmpl @@ -6,5 +6,8 @@ {{if eq .PageType "variables"}} {{template "shared/variables/variable_list" .}} {{end}} + {{if eq .PageType "scoped-workflows"}} + {{template "shared/actions/scoped_workflows" .}} + {{end}} {{template "admin/layout_footer" .}} diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index ae37950fb5..14fbf448fa 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -155,8 +155,6 @@
{{ctx.Locale.Tr "admin.config.default_allow_only_contributors_to_track_time"}}
{{svg (Iif .Service.DefaultAllowOnlyContributorsToTrackTime "octicon-check" "octicon-x")}}
{{end}} -
{{ctx.Locale.Tr "admin.config.default_visibility_organization"}}
-
{{.Service.DefaultOrgVisibility}}
{{ctx.Locale.Tr "admin.config.no_reply_address"}}
{{if .Service.NoReplyAddress}}{{.Service.NoReplyAddress}}{{else}}-{{end}}
diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl index b6c9f155e9..a54fa530e2 100644 --- a/templates/admin/navbar.tmpl +++ b/templates/admin/navbar.tmpl @@ -72,7 +72,7 @@ {{end}} {{end}} {{if .EnableActions}} -
+
{{ctx.Locale.Tr "actions.actions"}}
{{end}} diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index 800f8dac5b..bf71a12d1e 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -73,7 +73,7 @@ {{DateUtils.AbsoluteShort .Version.CreatedUnix}} {{svg "octicon-trash"}} diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index d366b8cb2d..02205793c4 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -86,7 +86,7 @@ {{DateUtils.AbsoluteShort .CreatedUnix}} {{svg "octicon-trash"}} diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index 58728fd117..33789130f8 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -9,10 +9,6 @@ {{/* required by: 1. "redirect_to" cookie; 2. cross-origin protection */}} -{{if .GoGetImport}} - - -{{end}} {{if and .EnableFeed .FeedURL}} diff --git a/templates/base/paginate.tmpl b/templates/base/paginate.tmpl index f6c1785ccf..2ed8834f34 100644 --- a/templates/base/paginate.tmpl +++ b/templates/base/paginate.tmpl @@ -1,7 +1,9 @@ -{{$paginationParams := .Page.GetParams}} +{{$page := or $.Page ctx.RootData.Page}} {{$paginationLink := $.Link}} +{{if eq $paginationLink nil}}{{$paginationLink = ctx.RootData.Link}}{{end}} {{if eq $paginationLink AppSubUrl}}{{$paginationLink = print $paginationLink "/"}}{{end}} -{{with .Page.Paginater}} +{{$paginationParams := $page.GetParams}} +{{with $page.Paginater}} {{if or (eq .TotalPages -1) (gt .TotalPages 1)}} {{$showFirstLast := gt .TotalPages 1}}
diff --git a/templates/devtest/flex-list.tmpl b/templates/devtest/flex-list.tmpl index 26df0920c5..2551941f7f 100644 --- a/templates/devtest/flex-list.tmpl +++ b/templates/devtest/flex-list.tmpl @@ -1,7 +1,17 @@ {{template "devtest/devtest-header"}}
-

Flex List (standalone)

+

Flex Relaxed List

+
+
+
+ left looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong + right +
+
+
+ +

Flex Divided List (standalone)

@@ -87,7 +97,7 @@
-

Flex List (with "ui segment")

+

Flex Divided List (with "ui segment")

item 1
@@ -101,7 +111,7 @@
item 2
-

Flex List (with "ui segment fitted", items have their own padding)

+

Flex Divided List (with "ui segment fitted", items have their own padding)

item 1
diff --git a/templates/mail/repo/issue/default.tmpl b/templates/mail/repo/issue/default.tmpl index 44cdc563ad..74b407a779 100644 --- a/templates/mail/repo/issue/default.tmpl +++ b/templates/mail/repo/issue/default.tmpl @@ -63,19 +63,18 @@
{{.RenderedContent}}
{{end -}} - {{if eq .ActionName "push"}} -
    - {{$repoURL := $.Comment.Issue.PullRequest.BaseRepo.HTMLURL}} - {{range $commit := $.Comment.Commits}} -
  • - - {{ShortSha $commit.ID.String}} - - {{$commit.MessageTitle}} -
  • - {{end}} -
- {{end}}

+ {{if eq .ActionName "push"}} +
    + {{$repoURL := $.Comment.Issue.PullRequest.BaseRepo.HTMLURL}} + {{range $commit := $.Comment.Commits}} + {{$gitCommit := $commit.UserCommit.GitCommit}} +
  • + {{ShortSha $gitCommit.ID.String}} - {{$gitCommit.MessageTitle}} +
  • + {{end}} +
+ {{end}}

--- diff --git a/templates/mail/repo/release.tmpl b/templates/mail/repo/release.tmpl index 9e7295d370..bd4a525322 100644 --- a/templates/mail/repo/release.tmpl +++ b/templates/mail/repo/release.tmpl @@ -29,26 +29,26 @@ ---
{{.locale.Tr "mail.release.downloads"}} -

+

--- diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl index 4f9e54b610..58300edc5b 100644 --- a/templates/org/header.tmpl +++ b/templates/org/header.tmpl @@ -1,6 +1,6 @@ -

+
{{ctx.AvatarUtils.Avatar .Org 100}}
-
+
{{.Org.DisplayName}} diff --git a/templates/org/settings/actions.tmpl b/templates/org/settings/actions.tmpl index 1abf895627..87ea0cd65e 100644 --- a/templates/org/settings/actions.tmpl +++ b/templates/org/settings/actions.tmpl @@ -6,6 +6,8 @@ {{template "shared/secrets/add_list" .}} {{else if eq .PageType "variables"}} {{template "shared/variables/variable_list" .}} + {{else if eq .PageType "scoped-workflows"}} + {{template "shared/actions/scoped_workflows" .}} {{end}}
{{template "org/settings/layout_footer" .}} diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl index b14b4248fc..5239a20326 100644 --- a/templates/org/settings/navbar.tmpl +++ b/templates/org/settings/navbar.tmpl @@ -26,7 +26,7 @@ {{end}} {{if .EnableActions}} -
+
{{ctx.Locale.Tr "actions.actions"}}
{{end}} diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl index c652d9c481..2b51764cb4 100644 --- a/templates/org/team/members.tmpl +++ b/templates/org/team/members.tmpl @@ -35,7 +35,7 @@
{{if and $.IsOrganizationOwner (not (and ($.Team.IsOwnerTeam) (eq (len $.Team.Members) 1)))}} {{end}} diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl index a0b936285e..d2c092c1da 100644 --- a/templates/org/team/sidebar.tmpl +++ b/templates/org/team/sidebar.tmpl @@ -13,7 +13,7 @@
{{if .Team.IsMember ctx $.SignedUser.ID}} {{else if .IsOrganizationOwner}} diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl index 33524e2cca..9f8030f28c 100644 --- a/templates/org/team/teams.tmpl +++ b/templates/org/team/teams.tmpl @@ -37,7 +37,7 @@ {{.NumRepos}} {{ctx.Locale.Tr "org.lower_repositories"}} {{if .IsMember ctx $.SignedUser.ID}} {{end}} diff --git a/templates/repo/actions/list.tmpl b/templates/repo/actions/list.tmpl index bf2a1db0a7..25629a6994 100644 --- a/templates/repo/actions/list.tmpl +++ b/templates/repo/actions/list.tmpl @@ -1,4 +1,8 @@ {{template "base/head" .}} + +{{$showCreateWorkflowBadge := Iif .WorkflowBadge true false}} +{{$showEnableDisableWorkflow := and .AllowDisableOrEnableWorkflow .CurWorkflowIsListed $.CurWorkflow}} +
{{template "repo/header" .}}
@@ -10,18 +14,36 @@ - + {{ctx.Locale.Tr "actions.runs.actors_no_select"}} {{range .Actors}} - + {{ctx.AvatarUtils.Avatar . 20}} {{.GetDisplayName}} {{end}} @@ -73,11 +95,11 @@ {{svg "octicon-search"}}
- + {{ctx.Locale.Tr "actions.runs.status_no_select"}} {{range .StatusInfoList}} - + {{template "repo/icons/action_status" (dict "Status" .StatusName)}} {{.DisplayedStatus}} @@ -95,24 +117,31 @@ {{svg "octicon-search"}}
- + {{ctx.Locale.Tr "actions.runs.branches_no_select"}} {{range .RunBranches}} - + {{.}} {{end}}
- {{if and .AllowDisableOrEnableWorkflow .CurWorkflowIsListed $.CurWorkflow}} + {{if or $showCreateWorkflowBadge $showEnableDisableWorkflow}} {{end}} @@ -124,7 +153,8 @@ {{end}}
- {{template "repo/actions/runs_list" .}} + {{template "repo/actions/runs_list" dict "ActionRunListData" $.ActionRunListData}} + {{template "base/paginate" dict "Page" $.Page}}
@@ -133,4 +163,53 @@ {{end}}
+ +{{if $showCreateWorkflowBadge}} + +{{end}} + {{template "base/footer" .}} diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl index 58dc222cf5..b1c7623531 100644 --- a/templates/repo/actions/runs_list.tmpl +++ b/templates/repo/actions/runs_list.tmpl @@ -1,12 +1,16 @@ -
- {{if not .Runs}} +{{$data := .ActionRunListData}} +
+ {{if not $data.ActionRuns}}
{{svg "octicon-no-entry" 48}} -

{{if $.IsFiltered}}{{ctx.Locale.Tr "actions.runs.no_results"}}{{else}}{{ctx.Locale.Tr "actions.runs.no_runs"}}{{end}}

+

{{if $data.IsFiltered}}{{ctx.Locale.Tr "actions.runs.no_results"}}{{else}}{{ctx.Locale.Tr "actions.runs.no_runs"}}{{end}}

{{end}} - {{range $run := .Runs}} -
+ {{range $run := $data.ActionRuns}} +
{{template "repo/icons/action_status" (dict "Status" $run.Status.String "IconVariant" "circle-fill")}} @@ -15,22 +19,22 @@
{{$title := or $run.Title (ctx.Locale.Tr "actions.runs.empty_commit_message")}} - {{ctx.RenderUtils.RenderCommitMessageLinkSubject $title $run.Link $.Repository}} + {{ctx.RenderUtils.RenderCommitMessageLinkSubject $title $run.Link $run.Repo}}
- {{$workflowName := index $.WorkflowNames $run.WorkflowID}} - {{if not $.CurWorkflow}}{{if $workflowName}}{{$workflowName}}{{else}}{{$run.WorkflowID}}{{end}} {{end}}#{{$run.Index}}: + {{$workflowName := index $data.WorkflowNames $run.WorkflowID}} + {{if not $data.CurWorkflow}}{{if $workflowName}}{{$workflowName}}{{else}}{{$run.WorkflowID}}{{end}} {{end}}#{{$run.Index}}: {{- if $run.ScheduleID -}} {{ctx.Locale.Tr "actions.runs.scheduled"}} {{- else -}} {{ctx.Locale.Tr "actions.runs.commit"}} - {{ShortSha $run.CommitSHA}} + {{ShortSha $run.CommitSHA}} {{ctx.Locale.Tr "actions.runs.pushed_by"}} {{$run.TriggerUser.GetDisplayName}} {{- end -}} - {{$errMsg := index $.RunErrors $run.ID}} + {{$errMsg := index $data.RunErrors $run.ID}} {{if $errMsg}} {{svg "octicon-alert" 16 "tw-text-red"}} @@ -38,7 +42,7 @@ {{end}}
-
+
{{if $run.IsRefDeleted}} {{$run.PrettyRef}} {{else}} @@ -52,12 +56,12 @@ {{svg "octicon-kebab-horizontal"}} -{{template "base/paginate" .}} diff --git a/templates/repo/actions/view_component.tmpl b/templates/repo/actions/view_component.tmpl index 0d508e69e5..6f62b1e651 100644 --- a/templates/repo/actions/view_component.tmpl +++ b/templates/repo/actions/view_component.tmpl @@ -26,6 +26,7 @@ data-locale-total-duration="{{ctx.Locale.Tr "actions.runs.total_duration"}}" data-locale-run-details="{{ctx.Locale.Tr "actions.runs.run_details"}}" data-locale-workflow-file="{{ctx.Locale.Tr "actions.runs.workflow_file"}}" + data-locale-workflow-file-no-permission="{{ctx.Locale.Tr "actions.runs.workflow_file_no_permission"}}" data-locale-status-unknown="{{ctx.Locale.Tr "actions.status.unknown"}}" data-locale-status-waiting="{{ctx.Locale.Tr "actions.status.waiting"}}" data-locale-status-running="{{ctx.Locale.Tr "actions.status.running"}}" @@ -38,6 +39,7 @@ data-locale-artifacts-title="{{ctx.Locale.Tr "artifacts"}}" data-locale-artifact-expired="{{ctx.Locale.Tr "expired"}}" data-locale-artifact-expires-at="{{ctx.Locale.Tr "artifact_expires_at"}}" + data-locale-artifact-expired-at="{{ctx.Locale.Tr "artifact_expired_at"}}" data-locale-confirm-delete-artifact="{{ctx.Locale.Tr "confirm_delete_artifact"}}" data-locale-show-timestamps="{{ctx.Locale.Tr "show_timestamps"}}" data-locale-show-log-seconds="{{ctx.Locale.Tr "show_log_seconds"}}" diff --git a/templates/repo/actions/workflow_dispatch.tmpl b/templates/repo/actions/workflow_dispatch.tmpl index ad9b0e94cc..92e6b75503 100644 --- a/templates/repo/actions/workflow_dispatch.tmpl +++ b/templates/repo/actions/workflow_dispatch.tmpl @@ -7,7 +7,7 @@