diff --git a/.github/actions/docker-dryrun/action.yml b/.github/actions/docker-dryrun/action.yml index d280ea26ce7..e9cd88d46f6 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@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Build regular image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.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@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . platforms: ${{ inputs.platform }} diff --git a/.github/workflows/agent-scan.yml b/.github/workflows/agent-scan.yml new file mode 100644 index 00000000000..2c84face622 --- /dev/null +++ b/.github/workflows/agent-scan.yml @@ -0,0 +1,113 @@ +name: AgentScan + +on: + # jobs only use pinned actions and never checkout code + pull_request_target: # zizmor: ignore[dangerous-triggers] + types: [opened, reopened, synchronize, edited] + +concurrency: + group: agent-scan-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + issues: write + pull-requests: write + +jobs: + agentscan: + runs-on: ubuntu-latest + steps: + - name: AgentScan + id: agentscan + uses: MatteoGabriele/agentscan-action@0a0c88109b5153dff2805f969f5060441efb7b65 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + skip-members: "dependabot[bot],renovate[bot], giteabot (backports)" + agent-scan-comment: false + + - name: Handle flagged PR + if: contains(fromJSON('["automation","mixed"]'), steps.agentscan.outputs.classification) || steps.agentscan.outputs.community-flagged == 'true' + env: + CLASSIFICATION: ${{ steps.agentscan.outputs.classification }} + COMMUNITY_FLAGGED: ${{ steps.agentscan.outputs.community-flagged }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 + with: + script: | + const core = require('@actions/core'); + const prNumber = context.payload.pull_request.number; + const classification = process.env.CLASSIFICATION; + const communityFlagged = process.env.COMMUNITY_FLAGGED === 'true'; + const shouldClose = classification === 'automation' || communityFlagged; + + const issue = context.payload.pull_request; + const labels = issue.labels?.map(l => l.name) || []; + + if (!labels.includes('possible bot')) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: ['possible bot'], + }); + } + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + }); + + const alreadyCommented = comments.some(c => c.user.type === 'Bot' && c.body.includes('AI Contribution Policy')); + + if (!alreadyCommented) { + const closingNote = shouldClose + ? "We're closing this for now as the account looks automated. If we got that wrong, please just reopen the PR and we'll take another look." + : 'If this was flagged in error, we apologise! 😳 Just let us know. 🙏'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: [ + "We've flagged this pull request as potentially AI-assisted.", + '', + 'Gitea welcomes the thoughtful use of AI tools, but contributors must use them responsibly and clearly disclose any assistance. Please follow the AI Contribution Policy in `CONTRIBUTING.md` and update this PR accordingly:', + '', + 'Maintainers may close PRs that do not disclose AI assistance, appear to be low-quality AI-generated content, or where the contributor cannot explain the changes.', + '', + 'See: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md#ai-contribution-policy', + '', + closingNote, + ].join('\n'), + }); + } else { + core.info('Possible-bot comment already exists - skipping comment.'); + } + + if (shouldClose && issue.state === 'open' && !alreadyCommented) { + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + state: 'closed', + title: '🚨 unwelcome pr from bot 🚨', + }); + } + + const actionTaken = [ + 'Added `possible bot` label', + alreadyCommented ? null : 'posted policy comment', + shouldClose && !alreadyCommented ? 'closed PR' : null, + ].filter(Boolean).join(', '); + + core.summary + .addHeading('AgentScan: Possible Bot Flag', 2) + .addTable([ + [{ data: 'Property', header: true }, { data: 'Value', header: true }], + ['Pull Request', `#${prNumber}`], + ['Classification', classification], + ['Community flagged', String(communityFlagged)], + ['Action', actionTaken || 'No action (already handled)'], + ]) + .write(); diff --git a/.github/workflows/cache-seeder.yml b/.github/workflows/cache-seeder.yml index 8ec7adee07d..4e2988adb4e 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/go-setup with: lint-cache: "true" diff --git a/.github/workflows/cron-licenses.yml b/.github/workflows/cron-licenses.yml index edb6f2e1576..2d4e9262883 100644 --- a/.github/workflows/cron-licenses.yml +++ b/.github/workflows/cron-licenses.yml @@ -12,7 +12,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod diff --git a/.github/workflows/cron-renovate.yml b/.github/workflows/cron-renovate.yml index 2c6cc8aedcd..4db83a336dd 100644 --- a/.github/workflows/cron-renovate.yml +++ b/.github/workflows/cron-renovate.yml @@ -20,7 +20,7 @@ jobs: if: github.repository == 'go-gitea/gitea' # prevent running on forks timeout-minutes: 30 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: renovatebot/github-action@693b9ef15eec82123529a37c782242f091365961 # v46.1.14 with: renovate-version: ${{ env.RENOVATE_VERSION }} @@ -28,5 +28,5 @@ jobs: token: ${{ secrets.RENOVATE_TOKEN }} env: RENOVATE_BINARY_SOURCE: install # auto-install go/node toolchains needed by post-upgrade tasks. - RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg nolyfill)$"]' + RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg)$"]' RENOVATE_REPOSITORIES: '["go-gitea/gitea"]' diff --git a/.github/workflows/cron-translations.yml b/.github/workflows/cron-translations.yml index 17f29d4e0c5..7c215b2c176 100644 --- a/.github/workflows/cron-translations.yml +++ b/.github/workflows/cron-translations.yml @@ -12,7 +12,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2 with: upload_sources: true diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index c17afbca97f..3c0603974e4 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -49,7 +49,7 @@ jobs: e2e: ${{ steps.changes.outputs.e2e }} shell: ${{ steps.changes.outputs.shell }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 id: changes with: diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 801966e1444..d8129fd5b7a 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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 641a3cacb8b..4cc8d25bbb9 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/go-setup - uses: ./.github/actions/pgsql-shard with: @@ -78,7 +78,7 @@ jobs: ports: - "9000:9000" steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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:48e78eb9d1e1adcfb10184b2cc3c7fc5ed21e5a3be08875f239257d194bab8c9 + image: redis:latest@sha256:e74c9b933d78e2829583d88f92793f4524752a15ac59c8baff2dd5ed000b7432 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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 43a4f48669d..f7483132b5b 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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 f0283f40227..bcd5eba381e 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - 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 34395c8d9e3..dd190551624 100644 --- a/.github/workflows/pull-labeler.yml +++ b/.github/workflows/pull-labeler.yml @@ -30,7 +30,7 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.base.sha }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 diff --git a/.github/workflows/release-nightly-snapcraft.yml b/.github/workflows/release-nightly-snapcraft.yml index 0f9ac1d423b..46ea663f838 100644 --- a/.github/workflows/release-nightly-snapcraft.yml +++ b/.github/workflows/release-nightly-snapcraft.yml @@ -17,7 +17,7 @@ jobs: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Install snapcraft run: sudo snap install snapcraft --classic diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 82ebf79a61e..70251bb0910 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -14,7 +14,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # 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 @@ -57,7 +57,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -75,12 +75,12 @@ jobs: contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # 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@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Get cleaned branch name id: clean_name env: @@ -88,7 +88,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@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 id: meta with: images: |- @@ -98,7 +98,7 @@ jobs: type=raw,value=${{ steps.clean_name.outputs.branch }} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 id: meta_rootless with: images: |- @@ -112,18 +112,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular docker image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -133,7 +133,7 @@ jobs: cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max - name: build rootless docker image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.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 b246d87b9ad..34ed45b281f 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -15,7 +15,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # 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 @@ -58,7 +58,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -86,13 +86,13 @@ jobs: contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # 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@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + - 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 id: meta with: images: |- @@ -105,7 +105,7 @@ jobs: type=semver,pattern={{version}} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 id: meta_rootless with: images: |- @@ -121,18 +121,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular container image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -140,7 +140,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} annotations: ${{ steps.meta.outputs.annotations }} - name: build rootless container image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.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 9eb910c8e36..394c524b755 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -18,7 +18,7 @@ jobs: contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # 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 @@ -61,7 +61,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -89,13 +89,13 @@ jobs: contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # 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@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + - 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 id: meta with: images: |- @@ -112,7 +112,7 @@ jobs: type=semver,pattern={{major}}.{{minor}} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 id: meta_rootless with: images: |- @@ -133,18 +133,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular container image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -152,7 +152,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} annotations: ${{ steps.meta.outputs.annotations }} - name: build rootless container image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 diff --git a/MAINTAINERS b/MAINTAINERS index 26192eff447..03ff6999f33 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -64,4 +64,4 @@ metiftikci (@metiftikci) Christopher Homberger (@ChristopherHX) Tobias Balle-Petersen (@tobiasbp) TheFox (@TheFox0x7) -Nicolas (@bircni) \ No newline at end of file +Nicolas (@bircni) diff --git a/Makefile b/Makefile index b8cea6b891c..939264233fc 100644 --- a/Makefile +++ b/Makefile @@ -11,12 +11,12 @@ COMMA := , XGO_VERSION := go-1.26.x -AIR_PACKAGE ?= github.com/air-verse/air@v1.65.2 # renovate: datasource=go -EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.6.1 # renovate: datasource=go +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 GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go -SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.2 # renovate: datasource=go +SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.34.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 ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go @@ -608,14 +608,6 @@ update-js: node_modules ## update js dependencies rm -rf node_modules pnpm-lock.yaml pnpm install @touch node_modules - $(MAKE) --no-print-directory nolyfill - -.PHONY: nolyfill -nolyfill: node_modules ## apply nolyfill overrides to package.json and relock - pnpm exec nolyfill install - node tools/migrate-nolyfills.ts - pnpm install - @touch node_modules .PHONY: update-py update-py: node_modules ## update py dependencies diff --git a/assets/go-licenses.json b/assets/go-licenses.json index a3f66c6b5be..cfb195ebcc5 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -644,19 +644,14 @@ "path": "github.com/golang/snappy/LICENSE", "licenseText": "Copyright (c) 2011 The Snappy-Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, - { - "name": "github.com/google/btree", - "path": "github.com/google/btree/LICENSE", - "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, { "name": "github.com/google/flatbuffers", "path": "github.com/google/flatbuffers/LICENSE", "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { - "name": "github.com/google/go-github/v87", - "path": "github.com/google/go-github/v87/LICENSE", + "name": "github.com/google/go-github/v88", + "path": "github.com/google/go-github/v88/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" }, { diff --git a/cmd/generate.go b/cmd/generate.go index 01be73c2d11..239b75ba9c2 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -6,10 +6,12 @@ package cmd import ( "context" + "errors" "fmt" "os" "gitea.dev/modules/generate" + "gitea.dev/modules/ssh" "github.com/mattn/go-isatty" "github.com/urfave/cli/v3" @@ -21,6 +23,7 @@ func newGenerateCommand() *cli.Command { Usage: "Generate Gitea's secrets/keys/tokens", Commands: []*cli.Command{ newGenerateSecretCommand(), + newGenerateSSHCommand(), }, } } @@ -37,6 +40,17 @@ func newGenerateSecretCommand() *cli.Command { } } +func newGenerateSSHCommand() *cli.Command { + return &cli.Command{ + Name: "ssh", + Usage: "Generate ssh keys", + Commands: []*cli.Command{ + newGenerateSSHKeyCommand(), + newGenerateSSHHostKeysCommand(), + }, + } +} + func newGenerateInternalTokenCommand() *cli.Command { return &cli.Command{ Name: "INTERNAL_TOKEN", @@ -62,6 +76,30 @@ func newGenerateSecretKeyCommand() *cli.Command { } } +func newGenerateSSHKeyCommand() *cli.Command { + return &cli.Command{ + Name: "key", + Usage: "Generate a new ssh key", + Flags: []cli.Flag{ + &cli.IntFlag{Name: "bits", Aliases: []string{"b"}, Usage: "Number of bits in the key, ignored when key is ed25519"}, + &cli.StringFlag{Name: "type", Aliases: []string{"t"}, Value: "ed25519", Usage: "Specifies the type of key to create."}, + &cli.StringFlag{Name: "file", Aliases: []string{"f"}, Usage: "Specifies the path or base directory for the key file", Required: true}, + }, + Action: runGenerateKeyPair, + } +} + +func newGenerateSSHHostKeysCommand() *cli.Command { + return &cli.Command{ + Name: "host-keys", + Usage: "Generate host keys of all default key types (rsa, ecdsa, and ed25519) if they do not already exist.", + Flags: []cli.Flag{ + &cli.StringFlag{Name: "dir", Aliases: []string{"d"}, Usage: "Specifies the base directory for the key files", Required: true}, + }, + Action: runGenerateHostKey, + } +} + func runGenerateInternalToken(_ context.Context, c *cli.Command) error { internalToken, err := generate.NewInternalToken() if err != nil { @@ -103,3 +141,41 @@ func runGenerateSecretKey(_ context.Context, c *cli.Command) error { return nil } + +func runGenerateHostKey(_ context.Context, c *cli.Command) error { + file := c.String("dir") + info, err := os.Stat(file) + if errors.Is(err, os.ErrNotExist) { + if err = os.MkdirAll(file, 0o644); err != nil { + return err + } + } else if err != nil { + return err + } else if !info.IsDir() { + return errors.New("file already exists and is not a directory") + } + fmt.Fprintf(c.Writer, "Generating host keys in %s\n", file) + _, err = ssh.InitDefaultHostKeys(file) + return err +} + +func runGenerateKeyPair(_ context.Context, c *cli.Command) error { + file := c.String("file") + keyType := c.String("type") + + fmt.Fprintf(c.Writer, "Generating public/private %s key pair.\n", keyType) + + // Check if file exists to prevent overwriting + if _, err := os.Stat(file); err == nil { + if !confirm(c.Reader, c.Writer, "%s already exists.\nOverwrite (y/n)? ", file) { + fmt.Println("Aborting") + return nil + } + } + bits := c.Int("bits") + err := ssh.GenKeyPair(file, generate.SSHKeyType(keyType), bits) + if err == nil { + fmt.Printf("Your SSH key has been saved in %s\n", file) + } + return err +} diff --git a/cmd/helper.go b/cmd/helper.go index 9150e1c2338..37b20104379 100644 --- a/cmd/helper.go +++ b/cmd/helper.go @@ -38,22 +38,15 @@ func argsSet(c *cli.Command, args ...string) error { } // confirm waits for user input which confirms an action -func confirm() (bool, error) { +func confirm(stdin io.Reader, stdout io.Writer, msg string, args ...any) bool { var response string - - _, err := fmt.Scanln(&response) - if err != nil { - return false, err - } - + _, _ = fmt.Fprintf(stdout, msg, args...) + _, _ = fmt.Fscanln(stdin, &response) switch strings.ToLower(response) { case "y", "yes": - return true, nil - case "n", "no": - return false, nil - default: - return false, errors.New(response + " isn't a correct confirmation string") + return true } + return false } func initDB(ctx context.Context) error { diff --git a/cmd/mailer.go b/cmd/mailer.go index 61bd66c963a..d7b2f6b7bb1 100644 --- a/cmd/mailer.go +++ b/cmd/mailer.go @@ -22,14 +22,10 @@ func runSendMail(ctx context.Context, c *cli.Command) error { if !confirmSkipped { if len(body) == 0 { - fmt.Print("warning: Content is empty") + fmt.Println("warning: Content is empty") } - fmt.Print("Proceed with sending email? [Y/n] ") - isConfirmed, err := confirm() - if err != nil { - return err - } else if !isConfirmed { + if !confirm(c.Reader, c.Writer, "Proceed with sending email? [Y/n] ") { fmt.Println("The mail was not sent") return nil } diff --git a/cmd/serv.go b/cmd/serv.go index 93009cd32a3..b39076f6a78 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -113,23 +113,25 @@ func handleCliResponseExtra(extra private.ResponseExtra) error { return nil } -func getAccessMode(verb, lfsVerb string) perm.AccessMode { +// getAccessMode maps an SSH git/LFS verb to the access mode it requires, with +// ok=false for an unrecognised verb. Callers MUST reject the request when ok is +// false: AccessModeNone would otherwise pass the `userMode < mode` permission +// check in routers/private/serv.go and grant access. +func getAccessMode(verb, lfsVerb string) (mode perm.AccessMode, ok bool) { switch verb { case git.CmdVerbUploadPack, git.CmdVerbUploadArchive: - return perm.AccessModeRead + return perm.AccessModeRead, true case git.CmdVerbReceivePack: - return perm.AccessModeWrite + return perm.AccessModeWrite, true case git.CmdVerbLfsAuthenticate, git.CmdVerbLfsTransfer: switch lfsVerb { case git.CmdSubVerbLfsUpload: - return perm.AccessModeWrite + return perm.AccessModeWrite, true case git.CmdSubVerbLfsDownload: - return perm.AccessModeRead + return perm.AccessModeRead, true } } - // should be unreachable - setting.PanicInDevOrTesting("unknown verb: %s %s", verb, lfsVerb) - return perm.AccessModeNone + return perm.AccessModeNone, false } func runServ(ctx context.Context, c *cli.Command) error { @@ -247,7 +249,10 @@ func runServ(ctx context.Context, c *cli.Command) error { } } - requestedMode := getAccessMode(verb, lfsVerb) + requestedMode, ok := getAccessMode(verb, lfsVerb) + if !ok { + return fail(ctx, "Unknown git command", "Unknown git command %s %s", verb, lfsVerb) + } results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb) if extra.HasError() { diff --git a/cmd/serv_test.go b/cmd/serv_test.go new file mode 100644 index 00000000000..3bdcba1df37 --- /dev/null +++ b/cmd/serv_test.go @@ -0,0 +1,56 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "testing" + + "gitea.dev/models/perm" + "gitea.dev/modules/git" + + "github.com/stretchr/testify/assert" +) + +func TestGetAccessMode(t *testing.T) { + cases := []struct { + verb, lfsVerb string + expected perm.AccessMode + }{ + {git.CmdVerbUploadPack, "", perm.AccessModeRead}, + {git.CmdVerbUploadArchive, "", perm.AccessModeRead}, + {git.CmdVerbReceivePack, "", perm.AccessModeWrite}, + {git.CmdVerbLfsAuthenticate, git.CmdSubVerbLfsUpload, perm.AccessModeWrite}, + {git.CmdVerbLfsAuthenticate, git.CmdSubVerbLfsDownload, perm.AccessModeRead}, + {git.CmdVerbLfsTransfer, git.CmdSubVerbLfsUpload, perm.AccessModeWrite}, + {git.CmdVerbLfsTransfer, git.CmdSubVerbLfsDownload, perm.AccessModeRead}, + } + for _, tc := range cases { + t.Run(tc.verb+"/"+tc.lfsVerb, func(t *testing.T) { + mode, ok := getAccessMode(tc.verb, tc.lfsVerb) + assert.True(t, ok) + assert.Equal(t, tc.expected, mode) + }) + } +} + +// TestGetAccessModeUnknownVerb locks in the invariant that getAccessMode reports +// ok=false for unrecognised verbs and LFS sub-verbs, so runServ rejects them. An +// unknown verb has no valid access mode; if it were treated as AccessModeNone (0) +// it would pass the `userMode < mode` permission check in routers/private/serv.go +// and hand out valid LFS JWTs for any private repository. +func TestGetAccessModeUnknownVerb(t *testing.T) { + cases := []struct{ verb, lfsVerb string }{ + {git.CmdVerbLfsAuthenticate, ""}, + {git.CmdVerbLfsAuthenticate, "badverb"}, + {git.CmdVerbLfsTransfer, "badverb"}, + {"git-unknown-verb", ""}, + } + for _, tc := range cases { + t.Run(tc.verb+"/"+tc.lfsVerb, func(t *testing.T) { + mode, ok := getAccessMode(tc.verb, tc.lfsVerb) + assert.False(t, ok) + assert.Equal(t, perm.AccessModeNone, mode) + }) + } +} diff --git a/docs/community-governance.md b/docs/community-governance.md index e37e9def64e..0d9a9835a78 100644 --- a/docs/community-governance.md +++ b/docs/community-governance.md @@ -2,6 +2,36 @@ This document describes maintainer expectations, project governance, and the detailed pull request review workflow (labels, merge queue, commit message format for mergers). For what contributors should do when opening and updating a PR, see [CONTRIBUTING.md](../CONTRIBUTING.md). +## Table of contents + +- [Community governance and review process](#community-governance-and-review-process) + - [Table of contents](#table-of-contents) + - [Code review](#code-review) + - [Milestone](#milestone) + - [Labels](#labels) + - [Reviewing PRs](#reviewing-prs) + - [For reviewers](#for-reviewers) + - [Getting PRs merged](#getting-prs-merged) + - [Final call](#final-call) + - [Commit messages](#commit-messages) + - [PR Co-authors](#pr-co-authors) + - [PRs targeting `main`](#prs-targeting-main) + - [Backport PRs](#backport-prs) + - [Contribution Roles](#contribution-roles) + - [Maintainers](#maintainers) + - [Review expectations](#review-expectations) + - [Becoming a maintainer](#becoming-a-maintainer) + - [Stepping down, advisors, and inactivity](#stepping-down-advisors-and-inactivity) + - [Account security](#account-security) + - [Mergers](#mergers) + - [Becoming a merger](#becoming-a-merger) + - [Technical Oversight Committee (TOC)](#technical-oversight-committee-toc) + - [TOC election process](#toc-election-process) + - [Current TOC members](#current-toc-members) + - [Previous TOC/owners members](#previous-tocowners-members) + - [Governance Compensation](#governance-compensation) + - [Roadmap](#roadmap) + ## Code review ### Milestone @@ -92,7 +122,9 @@ $PR_TITLE ($INITIAL_PR_INDEX) ($BACKPORT_PR_INDEX) $REWRITTEN_PR_SUMMARY ``` -## Maintainers +## Contribution Roles + +### Maintainers We list [maintainers](../MAINTAINERS) so every PR gets proper review. @@ -121,12 +153,25 @@ For security, maintainers should enable 2FA and sign commits with GPG when possi Any account with write access (including bots and TOC members) **must** use [2FA](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication). -## Technical Oversight Committee (TOC) +### Mergers + +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. +- 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)). + +#### Becoming a merger + +A merger should already be a Gitea maintainer. To apply, use the [Discord](https://discord.gg/Gitea) `#maintainers` channel. Mergers teams may also invite contributors. + +### Technical Oversight Committee (TOC) At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions are elected as it has been over the past years, and the other three consist of appointed members from the Gitea company. https://blog.gitea.com/quarterly-23q1/ -### TOC election process +#### TOC election process Any maintainer is eligible to be part of the community TOC if they are not associated with the Gitea company. A maintainer can either nominate themselves, or can be nominated by other maintainers to be a candidate for the TOC election. @@ -142,7 +187,7 @@ If an elected member that accepts the seat does not have 2FA configured yet, the ### Current TOC members -- 2024-01-01 ~ 2024-12-31 +- 2025-01-01 ~ 2026-06-14 - Company - [Jason Song](https://gitea.com/wolfogre) - [Lunny Xiao](https://gitea.com/lunny) @@ -150,7 +195,7 @@ If an elected member that accepts the seat does not have 2FA configured yet, the - Community - [6543](https://gitea.com/6543) <6543@obermui.de> - [delvh](https://gitea.com/delvh) - - [John Olheiser](https://gitea.com/jolheiser) + - [lafriks](https://gitea.com/lafriks) ### Previous TOC/owners members @@ -163,7 +208,7 @@ Here's the history of the owners and the time they served: - [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 - [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 - [6543](https://gitea.com/6543) - 2023 -- [John Olheiser](https://gitea.com/jolheiser) - 2023 +- [John Olheiser](https://gitea.com/jolheiser) - 2023, 2024 - [Jason Song](https://gitea.com/wolfogre) - 2023 ## Governance Compensation @@ -176,18 +221,6 @@ These funds will come from community sources like the OpenCollective rather than Only non-company members are eligible for this compensation, and if a member of the community TOC takes the responsibility of release manager, they would only be compensated for their TOC duties. Gitea Ltd employees are not eligible to receive any funds from the OpenCollective unless it is reimbursement for a purchase made for the Gitea project itself. -## TOC & Working groups - -With Gitea covering many projects outside of the main repository, several groups will be created to help focus on specific areas instead of requiring maintainers to be a jack-of-all-trades. Maintainers are of course more than welcome to be part of multiple groups should they wish to contribute in multiple places. - -The currently proposed groups are: - -- **Core Group**: maintain the primary Gitea repository -- **Integration Group**: maintain the Gitea ecosystem's related tools, including go-sdk/tea/changelog/bots etc. -- **Documentation Group**: maintain related documents and repositories -- **Translation Group**: coordinate with translators and maintain translations -- **Security Group**: managed by TOC directly, members are decided by TOC, maintains security patches/responsible for security items - ## Roadmap Each year a roadmap will be discussed with the entire Gitea maintainers team, and feedback will be solicited from various stakeholders. diff --git a/go.mod b/go.mod index 37a2ef02fe8..25e319aefeb 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347 github.com/ProtonMail/go-crypto v1.4.1 github.com/PuerkitoBio/goquery v1.12.0 - github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.3 + github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0 github.com/alecthomas/chroma/v2 v2.25.0 github.com/aws/aws-sdk-go-v2/credentials v1.19.17 github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.14 @@ -57,7 +57,7 @@ require ( 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/v87 v87.0.0 + github.com/google/go-github/v88 v88.0.0 github.com/google/licenseclassifier/v2 v2.0.0 github.com/google/pprof v0.0.0-20260507013755-92041b743c96 github.com/google/uuid v1.6.0 @@ -193,7 +193,6 @@ require ( github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/snappy v1.0.0 // indirect - github.com/google/btree v1.1.3 // indirect github.com/google/flatbuffers v25.12.19+incompatible // indirect github.com/google/go-querystring v1.2.0 // indirect github.com/google/go-tpm v0.9.8 // indirect diff --git a/go.sum b/go.sum index 41ed9accca0..687b2458812 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/RoaringBitmap/roaring/v2 v2.16.0 h1:Kys1UNf49d5W8Tq3bpuAhIr/Z8/yPB+59 github.com/RoaringBitmap/roaring/v2 v2.16.0/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4= github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= -github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.3 h1:ikrUPushSxbpr9mmDhSgz1KyUTChjwkDrxY/jzv2OTQ= -github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.3/go.mod h1:bnXbvnI9Mfqdj4L3Y9aCsB1A/ztuYLNRzgVKsJFgR/U= +github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0 h1:LvK7+C6qgz8BPnmn7xGekf8vTkcqTvyBBHaLYIMxx0g= +github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0/go.mod h1:I28hc9eaiqKCoOB+9Wh/P1IOScfm0xCgyWC6DAo0lmo= 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= @@ -365,8 +365,6 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8= github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= -github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= -github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= @@ -378,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/v87 v87.0.0 h1:9Ck3dcOxWJyfsN8tzdah4YvmqB/7ZsstMglv/PkOsl0= -github.com/google/go-github/v87 v87.0.0/go.mod h1:hGUoT5pwm/ck5uLL+wroSVQfg8mpe+buxllCcGV4VaM= +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-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= diff --git a/models/actions/run_job.go b/models/actions/run_job.go index caf66ca451c..df01546fd84 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -4,6 +4,7 @@ package actions import ( + "cmp" "context" "fmt" "slices" @@ -671,18 +672,18 @@ func cancelOneJob(ctx context.Context, job *ActionRunJob) (*ActionRunJob, error) func cancelReusableCaller(ctx context.Context, caller *ActionRunJob) ([]*ActionRunJob, error) { cancelledJobs := make([]*ActionRunJob, 0) - if c, err := cancelOneJob(ctx, caller); err != nil { - return cancelledJobs, err - } else if c != nil { - cancelledJobs = append(cancelledJobs, c) - } - attemptJobs, err := GetRunJobsByRunAndAttemptID(ctx, caller.RunID, caller.RunAttemptID) if err != nil { return cancelledJobs, err } - for _, c := range CollectAllDescendantJobs(caller, attemptJobs) { + // Cancel descendants deepest-first, then the caller: a caller's status is aggregated from its children, + // so each child must reach its final state before its parent caller is re-aggregated. + // A child's ID always exceeds its parent's, so descending ID is a valid deepest-first order. + descendants := CollectAllDescendantJobs(caller, attemptJobs) + slices.SortFunc(descendants, func(a, b *ActionRunJob) int { return cmp.Compare(b.ID, a.ID) }) + + for _, c := range descendants { cancelled, err := cancelOneJob(ctx, c) if err != nil { return cancelledJobs, err @@ -691,5 +692,11 @@ func cancelReusableCaller(ctx context.Context, caller *ActionRunJob) ([]*ActionR cancelledJobs = append(cancelledJobs, cancelled) } } + + if c, err := cancelOneJob(ctx, caller); err != nil { + return cancelledJobs, err + } else if c != nil { + cancelledJobs = append(cancelledJobs, c) + } return cancelledJobs, nil } diff --git a/models/actions/run_job_summary.go b/models/actions/run_job_summary.go new file mode 100644 index 00000000000..63e913c7bba --- /dev/null +++ b/models/actions/run_job_summary.go @@ -0,0 +1,207 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + "gitea.dev/models/db" + "gitea.dev/modules/setting" + "gitea.dev/modules/timeutil" + "gitea.dev/modules/util" +) + +const ( + // JobSummaryCapability is the runner-declare capability string for job summaries. + JobSummaryCapability = "job-summary" + + // JobSummaryContentTypeMarkdown is the only accepted content type for job summaries. + JobSummaryContentTypeMarkdown = "text/markdown" + + // MaxJobSummarySize is the maximum accepted per-step summary payload size in bytes. + MaxJobSummarySize = 1024 * 1024 // 1 MiB + + // MaxJobSummaryAggregateSize is the maximum aggregate size of all step summaries within + // a single job attempt. Matches GitHub's documented per-job summary cap of 1 MiB. + MaxJobSummaryAggregateSize = 1024 * 1024 // 1 MiB +) + +// RunnerCapabilities returns the value advertised in the X-Gitea-Actions-Capabilities header. +// When more capabilities are added, return them comma-separated so runners can split on ", ". +func RunnerCapabilities() string { + return JobSummaryCapability +} + +type ActionRunJobSummary struct { + ID int64 `xorm:"pk autoincr"` + + RepoID int64 `xorm:"UNIQUE(summary_key)"` + RunID int64 `xorm:"UNIQUE(summary_key)"` + RunAttemptID int64 `xorm:"UNIQUE(summary_key) NOT NULL DEFAULT 0"` + JobID int64 `xorm:"UNIQUE(summary_key)"` + StepIndex int64 `xorm:"UNIQUE(summary_key)"` + + Content string `xorm:"LONGTEXT"` + ContentType string `xorm:"VARCHAR(255) NOT NULL DEFAULT 'text/markdown'"` + // ContentSize is the byte length of Content. Stored explicitly because LENGTH() + // counts characters (not bytes) on PostgreSQL, SQLite and MSSQL, which would let + // multibyte UTF-8 content bypass the aggregate cap. + ContentSize int64 `xorm:"NOT NULL DEFAULT 0"` + + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` +} + +func init() { + db.RegisterModel(new(ActionRunJobSummary)) +} + +func GetActionRunJobSummary(ctx context.Context, repoID, runID, runAttemptID, jobID, stepIndex int64) (*ActionRunJobSummary, error) { + var s ActionRunJobSummary + has, err := db.GetEngine(ctx). + Where("repo_id=? AND run_id=? AND run_attempt_id=? AND job_id=? AND step_index=?", repoID, runID, runAttemptID, jobID, stepIndex). + Get(&s) + if err != nil { + return nil, err + } + if !has { + return nil, util.ErrNotExist + } + return &s, nil +} + +// ErrJobSummaryAggregateExceeded is returned when a step summary upload would push the +// aggregate size of summaries for a single job attempt over MaxJobSummaryAggregateSize. +var ErrJobSummaryAggregateExceeded = util.NewInvalidArgumentErrorf("job summary aggregate size exceeded") + +func UpsertActionRunJobSummary(ctx context.Context, repoID, runID, runAttemptID, jobID, stepIndex int64, contentType string, content []byte) error { + if runID <= 0 || jobID <= 0 || repoID <= 0 || stepIndex < 0 { + return util.ErrInvalidArgument + } + if len(content) == 0 { + // Treat empty summaries as no-op; runner may create SUMMARY.md but never write to it. + return nil + } + if len(content) > MaxJobSummarySize { + return util.ErrInvalidArgument + } + if contentType != JobSummaryContentTypeMarkdown { + return util.ErrInvalidArgument + } + + // The aggregate check is best-effort: a tx wouldn't actually serialize concurrent + // step uploads (no row-level lock on the parent job), so wrapping these two + // statements only adds round-trip cost without changing the race semantics. + // The current step is excluded because the upsert below replaces its size with len(content). + otherSize, err := sumOtherJobSummarySizes(ctx, repoID, runID, runAttemptID, jobID, stepIndex) + if err != nil { + return err + } + if otherSize+int64(len(content)) > MaxJobSummaryAggregateSize { + return ErrJobSummaryAggregateExceeded + } + + now := timeutil.TimeStampNow() + return upsertActionRunJobSummary(ctx, &ActionRunJobSummary{ + RepoID: repoID, + RunID: runID, + RunAttemptID: runAttemptID, + JobID: jobID, + StepIndex: stepIndex, + Content: string(content), + ContentSize: int64(len(content)), + ContentType: contentType, + Created: now, + Updated: now, + }) +} + +// sumOtherJobSummarySizes returns the total stored size of all step summaries for a job +// except excludeStepIndex, computed in the database to avoid loading every row. +func sumOtherJobSummarySizes(ctx context.Context, repoID, runID, runAttemptID, jobID, excludeStepIndex int64) (int64, error) { + return db.GetEngine(ctx). + Where("repo_id=? AND run_id=? AND run_attempt_id=? AND job_id=? AND step_index<>?", repoID, runID, runAttemptID, jobID, excludeStepIndex). + SumInt(new(ActionRunJobSummary), "content_size") +} + +// DeleteActionRunJobSummary removes the stored summary for a specific step. Used when +// a runner PUTs an empty body to clear a previously-uploaded step summary. +func DeleteActionRunJobSummary(ctx context.Context, repoID, runID, runAttemptID, jobID, stepIndex int64) error { + _, err := db.GetEngine(ctx). + Where("repo_id=? AND run_id=? AND run_attempt_id=? AND job_id=? AND step_index=?", repoID, runID, runAttemptID, jobID, stepIndex). + Delete(new(ActionRunJobSummary)) + return err +} + +func upsertActionRunJobSummary(ctx context.Context, summary *ActionRunJobSummary) error { + engine := db.GetEngine(ctx) + columns := "`repo_id`, `run_id`, `run_attempt_id`, `job_id`, `step_index`, `content`, `content_type`, `content_size`, `created`, `updated`" + values := []any{ + summary.RepoID, + summary.RunID, + summary.RunAttemptID, + summary.JobID, + summary.StepIndex, + summary.Content, + summary.ContentType, + summary.ContentSize, + summary.Created, + summary.Updated, + } + + if setting.Database.Type.IsPostgreSQL() || setting.Database.Type.IsSQLite3() { + args := append([]any{"INSERT INTO `action_run_job_summary` (" + columns + ") VALUES (?,?,?,?,?,?,?,?,?,?) " + + "ON CONFLICT (`repo_id`, `run_id`, `run_attempt_id`, `job_id`, `step_index`) DO UPDATE SET " + + "`content` = excluded.`content`, `content_type` = excluded.`content_type`, `content_size` = excluded.`content_size`, `updated` = excluded.`updated`"}, values...) + _, err := engine.Exec(args...) + return err + } + + if setting.Database.Type.IsMySQL() { + args := append([]any{ + "INSERT INTO `action_run_job_summary` (" + columns + ") VALUES (?,?,?,?,?,?,?,?,?,?) " + + "ON DUPLICATE KEY UPDATE `content` = VALUES(`content`), `content_type` = VALUES(`content_type`), `content_size` = VALUES(`content_size`), `updated` = VALUES(`updated`)", + }, values...) + _, err := engine.Exec(args...) + return err + } + + if setting.Database.Type.IsMSSQL() { + _, err := engine.Exec(` +MERGE INTO action_run_job_summary WITH (HOLDLOCK) AS target +USING (SELECT ? AS repo_id, ? AS run_id, ? AS run_attempt_id, ? AS job_id, ? AS step_index) AS source +ON target.repo_id = source.repo_id + AND target.run_id = source.run_id + AND target.run_attempt_id = source.run_attempt_id + AND target.job_id = source.job_id + AND target.step_index = source.step_index +WHEN MATCHED THEN + UPDATE SET content = ?, content_type = ?, content_size = ?, updated = ? +WHEN NOT MATCHED THEN + INSERT (repo_id, run_id, run_attempt_id, job_id, step_index, content, content_type, content_size, created, updated) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); +`, + summary.RepoID, summary.RunID, summary.RunAttemptID, summary.JobID, summary.StepIndex, + summary.Content, summary.ContentType, summary.ContentSize, summary.Updated, + summary.RepoID, summary.RunID, summary.RunAttemptID, summary.JobID, summary.StepIndex, summary.Content, summary.ContentType, summary.ContentSize, summary.Created, summary.Updated) + return err + } + + return util.ErrInvalidArgument +} + +// ListActionRunJobSummaries lists the stored summaries for a run attempt, ordered by job +// then step. A positive jobID scopes the lookup to that single job, used by the job view to +// avoid rendering every job's summary on each poll; jobID<=0 returns all jobs in the attempt. +func ListActionRunJobSummaries(ctx context.Context, repoID, runID, runAttemptID, jobID int64) ([]*ActionRunJobSummary, error) { + sess := db.GetEngine(ctx).Where("repo_id=? AND run_id=? AND run_attempt_id=?", repoID, runID, runAttemptID) + if jobID > 0 { + sess = sess.And("job_id=?", jobID) + } + var summaries []*ActionRunJobSummary + if err := sess.OrderBy("job_id ASC, step_index ASC").Find(&summaries); err != nil { + return nil, err + } + return summaries, nil +} diff --git a/models/actions/run_job_test.go b/models/actions/run_job_test.go index a9e07ce0cf0..4437b5906df 100644 --- a/models/actions/run_job_test.go +++ b/models/actions/run_job_test.go @@ -131,3 +131,69 @@ func TestGetPriorAttemptChildrenByParent(t *testing.T) { assertAttempt1Children(t, out) }) } + +// A reusable caller subtree with a Blocked descendant (e.g. a nested caller stuck on an invalid `uses:`) must aggregate to Cancelled, when the run is cancelled. +func TestCancelJobs_NestedBlockedReusableCaller(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + ctx := t.Context() + + run := &ActionRun{ + Title: "cancel-nested-caller", + RepoID: 4, + Index: 9701, + OwnerID: 1, + WorkflowID: "caller.yaml", + TriggerUserID: 1, + Ref: "refs/heads/master", + CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0", + Event: "push", + TriggerEvent: "push", + EventPayload: "{}", + Status: StatusBlocked, + } + require.NoError(t, db.Insert(ctx, run)) + + attempt := &ActionRunAttempt{RepoID: run.RepoID, RunID: run.ID, Attempt: 1, TriggerUserID: 1, Status: StatusBlocked} + require.NoError(t, db.Insert(ctx, attempt)) + run.LatestAttemptID = attempt.ID + require.NoError(t, UpdateRun(ctx, run, "latest_attempt_id")) + + newJob := func(name string, attemptJobID, parentID int64, callUses string) *ActionRunJob { + job := &ActionRunJob{ + RunID: run.ID, + RunAttemptID: attempt.ID, + RepoID: run.RepoID, + OwnerID: run.OwnerID, + CommitSHA: run.CommitSHA, + Name: name, + JobID: name, + Attempt: 1, + Status: StatusBlocked, + AttemptJobID: attemptJobID, + IsReusableCaller: true, + CallUses: callUses, + ParentJobID: parentID, + } + require.NoError(t, db.Insert(ctx, job)) + return job + } + + // outer: a valid top-level caller that expanded; inner: a nested caller stuck Blocked (invalid uses, never expands). + outer := newJob("outer", 1, 0, "./.gitea/workflows/lib.yml") + inner := newJob("inner", 2, outer.ID, "https://other.example.com/o/r/.gitea/workflows/ci.yml@v1") + + // Cancel all jobs of the attempt, ordered by id (parent before child). + jobs, err := GetRunJobsByRunAndAttemptID(ctx, run.ID, attempt.ID) + require.NoError(t, err) + _, err = CancelJobs(ctx, jobs) + require.NoError(t, err) + + for _, j := range []*ActionRunJob{outer, inner} { + got := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: j.ID}) + assert.Equal(t, StatusCancelled, got.Status, "job %q should be cancelled", j.JobID) + } + gotAttempt := unittest.AssertExistsAndLoadBean(t, &ActionRunAttempt{ID: attempt.ID}) + assert.Equal(t, StatusCancelled, gotAttempt.Status, "attempt must aggregate to Cancelled") + gotRun := unittest.AssertExistsAndLoadBean(t, &ActionRun{ID: run.ID}) + assert.Equal(t, StatusCancelled, gotRun.Status, "run must aggregate to Cancelled, not stay Blocked") +} diff --git a/models/actions/run_list.go b/models/actions/run_list.go index 77de6a85127..553fd7bae99 100644 --- a/models/actions/run_list.go +++ b/models/actions/run_list.go @@ -64,7 +64,6 @@ type FindRunOptions struct { Ref string // the commit/tag/… that caused this workflow TriggerUserID int64 TriggerEvent webhook_module.HookEventType - Approved bool // not util.OptionalBool, it works only when it's true Status []Status ConcurrencyGroup string CommitSHA string @@ -81,9 +80,6 @@ func (opts FindRunOptions) ToConds() builder.Cond { if opts.TriggerUserID > 0 { cond = cond.And(builder.Eq{"`action_run`.trigger_user_id": opts.TriggerUserID}) } - if opts.Approved { - cond = cond.And(builder.Gt{"`action_run`.approved_by": 0}) - } if len(opts.Status) > 0 { cond = cond.And(builder.In("`action_run`.status", opts.Status)) } diff --git a/models/asymkey/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go index 17244076dd2..251d8eff11c 100644 --- a/models/asymkey/gpg_key_commit_verification.go +++ b/models/asymkey/gpg_key_commit_verification.go @@ -8,6 +8,7 @@ import ( "fmt" "hash" + "gitea.dev/models/gituser" repo_model "gitea.dev/models/repo" user_model "gitea.dev/models/user" "gitea.dev/modules/log" @@ -32,8 +33,8 @@ type CommitVerification struct { // SignCommit represents a commit with validation of signature. type SignCommit struct { - Verification *CommitVerification - *user_model.UserCommit + Verification *CommitVerification + *gituser.UserCommit // TODO: need to use a explicit field name, avoid anonymous field } const ( diff --git a/models/db/driver_postgresschema.go b/models/db/driver_postgresschema.go index ad2b5abc04a..616712fb2a0 100644 --- a/models/db/driver_postgresschema.go +++ b/models/db/driver_postgresschema.go @@ -4,8 +4,10 @@ package db import ( + "context" "database/sql" "database/sql/driver" + "errors" "sync" "gitea.dev/modules/setting" @@ -14,61 +16,34 @@ import ( "xorm.io/xorm/dialects" ) -var registerOnce sync.Once +type postgresSchemaDriver struct{} -func registerPostgresSchemaDriver() { - registerOnce.Do(func() { - sql.Register(sqlDriverPostgresSchema, &postgresSchemaDriver{}) - dialects.RegisterDriver(sqlDriverPostgresSchema, dialects.QueryDriver("postgres")) - }) -} +var registerPostgresSchemaDriver = sync.OnceFunc(func() { + sql.Register(sqlDriverPostgresSchema, &postgresSchemaDriver{}) + dialects.RegisterDriver(sqlDriverPostgresSchema, dialects.QueryDriver("postgres")) +}) -type postgresSchemaDriver struct { - pq.Driver -} - -// Open opens a new connection to the database. name is a connection string. -// This function opens the postgres connection in the default manner but immediately -// runs set_config to set the search_path appropriately -func (d *postgresSchemaDriver) Open(name string) (driver.Conn, error) { - conn, err := d.Driver.Open(name) +// Open opens the postgres connection in the default manner with default schema support. +// It immediately runs "set_config" to set the search_path appropriately. +func (*postgresSchemaDriver) Open(connStr string) (driver.Conn, error) { + conn, err := pq.Driver{}.Open(connStr) if err != nil { - return conn, err + return nil, err } - schemaValue, _ := driver.String.ConvertValue(setting.Database.Schema) - // golangci lint is incorrect here - there is no benefit to using driver.ExecerContext here - // and in any case pq does not implement it - if execer, ok := conn.(driver.Execer); ok { //nolint:staticcheck // see above - _, err := execer.Exec(`SELECT set_config( + connExec, ok := conn.(driver.ExecerContext) + if !ok { + return nil, errors.New("postgres driver does not implement ExecerContext interface") + } + _, err = connExec.ExecContext(context.Background(), `SELECT set_config( 'search_path', $1 || ',' || current_setting('search_path'), - false)`, []driver.Value{schemaValue}) - if err != nil { - _ = conn.Close() - return nil, err - } - return conn, nil - } - - stmt, err := conn.Prepare(`SELECT set_config( - 'search_path', - $1 || ',' || current_setting('search_path'), - false)`) + false)`, + []driver.NamedValue{{Ordinal: 1, Value: setting.Database.Schema}}, + ) if err != nil { _ = conn.Close() return nil, err } - defer stmt.Close() - - // driver.String.ConvertValue will never return err for string - - // golangci lint is incorrect here - there is no benefit to using stmt.ExecWithContext here - _, err = stmt.Exec([]driver.Value{schemaValue}) //nolint:staticcheck // see above - if err != nil { - _ = conn.Close() - return nil, err - } - return conn, nil } diff --git a/models/gituser/avatar_stack.go b/models/gituser/avatar_stack.go new file mode 100644 index 00000000000..d38c94bc7bf --- /dev/null +++ b/models/gituser/avatar_stack.go @@ -0,0 +1,44 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gituser + +import ( + "context" + + "gitea.dev/models/user" + "gitea.dev/modules/git" + "gitea.dev/modules/log" +) + +// AvatarStackData is the view-model for the AvatarStack render helpers. Participants[0] is +// the primary participant (commit author), painted on top; the rest follow. +type AvatarStackData struct { + Participants []*CommitParticipant + SearchByEmailLink string +} + +func BuildAvatarStackData(ctx context.Context, allParticipants []*git.CommitIdentity, emailUserMap *user.EmailUserMap) *AvatarStackData { + if emailUserMap == nil { + emails := make([]string, len(allParticipants)) + for i, sig := range allParticipants { + emails[i] = sig.Email + } + var err error + emailUserMap, err = user.GetUsersByEmails(ctx, emails) + if err != nil { + log.Error("GetUsersByEmails failed: %v", err) + } + } + ret := &AvatarStackData{ + Participants: make([]*CommitParticipant, 0, len(allParticipants)), + } + for _, p := range allParticipants { + var giteaUser *user.User + if emailUserMap != nil { + giteaUser = emailUserMap.GetByEmail(p.Email) + } + ret.Participants = append(ret.Participants, &CommitParticipant{GiteaUser: giteaUser, GitIdentity: p}) + } + return ret +} diff --git a/models/gituser/gituser.go b/models/gituser/gituser.go new file mode 100644 index 00000000000..81a94a3a85b --- /dev/null +++ b/models/gituser/gituser.go @@ -0,0 +1,64 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gituser + +import ( + "context" + "net/url" + + "gitea.dev/models/user" + "gitea.dev/modules/container" + "gitea.dev/modules/git" +) + +// CommitParticipant is one participant of a commit (its author or a co-author): +// a git identity, optionally matched to a Gitea user. +type CommitParticipant struct { + GitIdentity *git.CommitIdentity // git identity (name/email), never nil + GiteaUser *user.User // matched Gitea user, nil if unmatched +} + +// UserCommit represents a commit with matched of database "author" user. +type UserCommit struct { + GitCommit *git.Commit + AuthorUser *user.User + AvatarStackData *AvatarStackData +} + +func RepoCommitSearchByEmailLink(repoLink string, ref git.RefName) string { + if curRefWebLinkPath := ref.RefWebLinkPath(); curRefWebLinkPath != "" { + return repoLink + "/commits/" + curRefWebLinkPath + "/search?q=" + url.QueryEscape("author:") + "{email}" + } + return "" +} + +// GetUserCommitsByGitCommits checks if authors' e-mails of commits are corresponding to users. +func GetUserCommitsByGitCommits(ctx context.Context, gitCommits []*git.Commit, repoLink string, currentRef git.RefName) ([]*UserCommit, error) { + userCommits := make([]*UserCommit, 0, len(gitCommits)) + emailSet := make(container.Set[string]) + for _, c := range gitCommits { + emailSet.Add(c.Author.Email) + emailSet.Add(c.Committer.Email) + for _, p := range c.AllParticipantIdentities() { + emailSet.Add(p.Email) + } + } + + emailUserMap, err := user.GetUsersByEmails(ctx, emailSet.Values()) + if err != nil { + return nil, err + } + + searchByEmailLink := RepoCommitSearchByEmailLink(repoLink, currentRef) + for _, c := range gitCommits { + uc := &UserCommit{ + AuthorUser: emailUserMap.GetByEmail(c.Author.Email), // FIXME: why GetUserCommitsByGitCommits uses "Author", but ParseCommitsWithSignature uses "Committer"? + GitCommit: c, + AvatarStackData: BuildAvatarStackData(ctx, c.AllParticipantIdentities(), emailUserMap), + } + uc.AvatarStackData.SearchByEmailLink = searchByEmailLink + userCommits = append(userCommits, uc) + } + return userCommits, nil +} diff --git a/models/issues/pull.go b/models/issues/pull.go index 7dbcef0d3fe..2b93b926b01 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "strings" + "time" "gitea.dev/models/db" git_model "gitea.dev/models/git" @@ -860,6 +861,11 @@ func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRul return rules, warnings } +// codeOwnerMatchTimeout bounds a single pattern match so a crafted pattern +// cannot stall via catastrophic backtracking. See also the aggregate budget +// enforced by the caller across the whole rules×files match loop. +const codeOwnerMatchTimeout = 150 * time.Millisecond + type CodeOwnerRule struct { Rule *regexp2.Regexp // it supports negative lookahead, does better for end users Negative bool @@ -888,6 +894,8 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err)) return nil, warnings } + // Bound matching time so user-supplied patterns cannot stall PR creation via catastrophic backtracking. + rule.Rule.MatchTimeout = codeOwnerMatchTimeout for _, user := range tokens[1:] { user = strings.TrimPrefix(user, "@") diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index 166fdd8482b..bd7499fa80b 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -4,7 +4,9 @@ package issues_test import ( + "strings" "testing" + "time" "gitea.dev/models/db" issues_model "gitea.dev/models/issues" @@ -39,6 +41,7 @@ func TestPullRequest(t *testing.T) { t.Run("DeleteOrphanedObjects", testDeleteOrphanedObjects) t.Run("ParseCodeOwnersLine", testParseCodeOwnersLine) t.Run("CodeOwnerAbsolutePathPatterns", testCodeOwnerAbsolutePathPatterns) + t.Run("CodeOwnerPatternMatchTimeout", testCodeOwnerPatternMatchTimeout) t.Run("GetApprovers", testGetApprovers) t.Run("GetPullRequestByMergedCommit", testGetPullRequestByMergedCommit) t.Run("Migrate_InsertPullRequests", testMigrateInsertPullRequests) @@ -376,6 +379,22 @@ func testCodeOwnerAbsolutePathPatterns(t *testing.T) { } } +// testCodeOwnerPatternMatchTimeout ensures user-supplied CODEOWNERS patterns +// cannot stall pull request processing through catastrophic regex backtracking: +// each compiled rule must enforce a bounded match time. +func testCodeOwnerPatternMatchTimeout(t *testing.T) { + rules, _ := issues_model.GetCodeOwnersFromContent(t.Context(), "(a+)+ @user5\n") + require.Len(t, rules, 1) + + maliciousInput := strings.Repeat("a", 30) + "X" + start := time.Now() + _, err := rules[0].Rule.MatchString(maliciousInput) + elapsed := time.Since(start) + + require.Error(t, err, "expected MatchTimeout error on pathological input") + assert.Less(t, elapsed, time.Second, "match timeout did not bound regex evaluation; took %s", elapsed) +} + func testGetApprovers(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 5}) // Official reviews are already deduplicated. Allow unofficial reviews diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 904a3ffa205..aedd679c57f 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -413,6 +413,7 @@ func prepareMigrationTasks() []*migration { newMigration(333, "Add bypass allowlist to branch protection", v1_27.AddBranchProtectionBypassAllowlist), newMigration(334, "Add cancelling support to action runners", v1_27.AddCancellingSupportToActionRunner), newMigration(335, "Add reusable workflow fields and action_run_attempt_job_id_index table for ActionRunJob", v1_27.AddReusableWorkflowFieldsToActionRunJob), + newMigration(336, "Add ActionRunJobSummary table", v1_27.AddActionRunJobSummaryTable), } return preparedMigrations } diff --git a/models/migrations/v1_27/v336.go b/models/migrations/v1_27/v336.go new file mode 100644 index 00000000000..9c5e0c9494a --- /dev/null +++ b/models/migrations/v1_27/v336.go @@ -0,0 +1,30 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_27 + +import ( + "gitea.dev/models/db" + "gitea.dev/modules/timeutil" +) + +func AddActionRunJobSummaryTable(x db.EngineMigration) error { + type ActionRunJobSummary struct { + ID int64 `xorm:"pk autoincr"` + + RepoID int64 `xorm:"UNIQUE(summary_key)"` + RunID int64 `xorm:"UNIQUE(summary_key)"` + RunAttemptID int64 `xorm:"UNIQUE(summary_key) NOT NULL DEFAULT 0"` + JobID int64 `xorm:"UNIQUE(summary_key)"` + StepIndex int64 `xorm:"UNIQUE(summary_key)"` + + Content string `xorm:"LONGTEXT"` + ContentType string `xorm:"VARCHAR(255) NOT NULL DEFAULT 'text/markdown'"` + ContentSize int64 `xorm:"NOT NULL DEFAULT 0"` + + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` + } + + return x.Sync(new(ActionRunJobSummary)) +} diff --git a/models/repo/repo.go b/models/repo/repo.go index 7f8507722f0..e56603fc812 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -23,6 +23,7 @@ import ( "gitea.dev/modules/base" "gitea.dev/modules/git" giturl "gitea.dev/modules/git/url" + "gitea.dev/modules/htmlutil" "gitea.dev/modules/httplib" "gitea.dev/modules/log" "gitea.dev/modules/markup" @@ -641,12 +642,7 @@ func (repo *Repository) CanContentChange() bool { // DescriptionHTML does special handles to description and return HTML string. func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML { - desc, err := markup.PostProcessDescriptionHTML(markup.NewRenderContext(ctx), repo.Description) - if err != nil { - log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err) - return template.HTML(markup.SanitizeDescription(repo.Description)) - } - return template.HTML(markup.SanitizeDescription(desc)) + return markup.PostProcessDescriptionHTML(markup.NewRenderContext(ctx), htmlutil.EscapeString(repo.Description)) } // CloneLink represents different types of clone URLs of repository. diff --git a/models/unittest/unit_tests.go b/models/unittest/unit_tests.go index 45ea0d175ea..c9895500214 100644 --- a/models/unittest/unit_tests.go +++ b/models/unittest/unit_tests.go @@ -47,7 +47,7 @@ func OrderBy(orderBy string) any { } func whereOrderConditions(e db.Engine, conditions []any) db.Engine { - orderBy := "id" // query must have the "ORDER BY", otherwise the result is not deterministic + orderBy := "id" // query must have the "ORDER BY", otherwise the result is not deterministic. FIXME: some tables do not have "id" column for _, condition := range conditions { switch cond := condition.(type) { case *testCond: diff --git a/models/user/user.go b/models/user/user.go index 66e8d49b420..4e6227de9e7 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1148,14 +1148,7 @@ func GetUsersBySource(ctx context.Context, s *auth.Source) ([]*User, error) { return users, err } -// UserCommit represents a commit with validation of user. -type UserCommit struct { //revive:disable-line:exported - User *User - *git.Commit -} - -// ValidateCommitWithEmail check if author's e-mail of commit is corresponding to a user. -func ValidateCommitWithEmail(ctx context.Context, c *git.Commit) *User { +func GetUserByGitAuthor(ctx context.Context, c *git.Commit) *User { if c.Author == nil { return nil } @@ -1166,33 +1159,6 @@ func ValidateCommitWithEmail(ctx context.Context, c *git.Commit) *User { return u } -// ValidateCommitsWithEmails checks if authors' e-mails of commits are corresponding to users. -func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([]*UserCommit, error) { - var ( - newCommits = make([]*UserCommit, 0, len(oldCommits)) - emailSet = make(container.Set[string]) - ) - for _, c := range oldCommits { - if c.Author != nil { - emailSet.Add(c.Author.Email) - } - } - - emailUserMap, err := GetUsersByEmails(ctx, emailSet.Values()) - if err != nil { - return nil, err - } - - for _, c := range oldCommits { - user := emailUserMap.GetByEmail(c.Author.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"? - newCommits = append(newCommits, &UserCommit{ - User: user, - Commit: c, - }) - } - return newCommits, nil -} - type EmailUserMap struct { m map[string]*User } @@ -1203,7 +1169,7 @@ func (eum *EmailUserMap) GetByEmail(email string) *User { func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, error) { if len(emails) == 0 { - return nil, nil //nolint:nilnil // return nil when there are no emails to look up + return &EmailUserMap{}, nil } needCheckEmails := make(container.Set[string]) diff --git a/modules/consts/asymkey.go b/modules/consts/asymkey.go new file mode 100644 index 00000000000..d6f19b2c53c --- /dev/null +++ b/modules/consts/asymkey.go @@ -0,0 +1,12 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package consts + +const ( + AsymKeyMinBitsRsa = 3071 // 3072-1 to tolerate the leading zero + AsymKeyMinBitsEC = 256 + + AsymKeyDefaultBitsRsa = 4096 // ssh-keygen command defaults to 3072 + AsymKeyDefaultBitsEcdsa = 256 +) diff --git a/modules/generate/generate.go b/modules/generate/generate.go index f2a5b366d8f..ca132e0756c 100644 --- a/modules/generate/generate.go +++ b/modules/generate/generate.go @@ -5,15 +5,23 @@ package generate import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" "crypto/rand" + "crypto/rsa" "encoding/base64" + "encoding/pem" "fmt" "io" "time" + "gitea.dev/modules/consts" "gitea.dev/modules/util" "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/ssh" ) // NewInternalToken generate a new value intended to be used by INTERNAL_TOKEN. @@ -67,3 +75,75 @@ func NewJwtSecretWithBase64() ([]byte, string) { func NewSecretKey() (string, error) { return util.CryptoRandomString(64), nil } + +type SSHKeyType string + +const ( + SSHKeyRSA SSHKeyType = "rsa" + SSHKeyECDSA SSHKeyType = "ecdsa" + SSHKeyED25519 SSHKeyType = "ed25519" +) + +func NewSSHKey(keyType SSHKeyType, bits int) (ssh.PublicKey, *pem.Block, error) { + pub, priv, err := commonKeyGen(keyType, bits) + if err != nil { + return nil, nil, err + } + pemPriv, err := ssh.MarshalPrivateKey(priv, "") + if err != nil { + return nil, nil, err + } + sshPub, err := ssh.NewPublicKey(pub) + if err != nil { + return nil, nil, err + } + + return sshPub, pemPriv, nil +} + +// commonKeyGen is an abstraction over rsa, ecdsa, and ed25519 generating functions +func commonKeyGen(keyType SSHKeyType, bits int) (crypto.PublicKey, crypto.PrivateKey, error) { + switch keyType { + case SSHKeyRSA: + bits = util.IfZero(bits, consts.AsymKeyDefaultBitsRsa) + if bits < consts.AsymKeyMinBitsRsa { + return nil, nil, util.NewInvalidArgumentErrorf("invalid rsa bits: %d", bits) + } + privateKey, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, nil, err + } + return &privateKey.PublicKey, privateKey, nil + case SSHKeyED25519: + return ed25519.GenerateKey(rand.Reader) + case SSHKeyECDSA: + bits = util.IfZero(bits, consts.AsymKeyDefaultBitsEcdsa) + if bits < consts.AsymKeyMinBitsEC { + return nil, nil, util.NewInvalidArgumentErrorf("invalid elliptic-curve bits: %d", bits) + } + curve, err := getEllipticCurve(bits) + if err != nil { + return nil, nil, err + } + privateKey, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + return nil, nil, err + } + return &privateKey.PublicKey, privateKey, nil + default: + return nil, nil, util.NewInvalidArgumentErrorf("unknown key type: %s", keyType) + } +} + +func getEllipticCurve(bits int) (elliptic.Curve, error) { + switch bits { + case 256: + return elliptic.P256(), nil + case 384: + return elliptic.P384(), nil + case 521: + return elliptic.P521(), nil + default: + return nil, util.NewInvalidArgumentErrorf("unsupported elliptic-curve bits: %d", bits) + } +} diff --git a/modules/git/commit.go b/modules/git/commit.go index 0231770a293..21288ad8459 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -11,18 +11,10 @@ import ( "os/exec" "strings" - "gitea.dev/modules/charset" "gitea.dev/modules/git/gitcmd" "gitea.dev/modules/util" ) -type CommitMessage struct { - MessageRaw string - messageUTF8 *string - messageTitle *string - messageBody *string -} - // Commit represents a git commit. type Commit struct { Tree // FIXME: bad design, this field can be nil if the commit is from "last commit cache" @@ -44,30 +36,6 @@ type CommitSignature struct { Payload string } -func (c *CommitMessage) MessageUTF8() string { - if c.messageUTF8 == nil { - bs := charset.ToUTF8(util.UnsafeStringToBytes(c.MessageRaw), charset.ConvertOpts{ErrorReplacement: []byte{'?'}}) - c.messageUTF8 = new(util.UnsafeBytesToString(bs)) - } - return *c.messageUTF8 -} - -func (c *CommitMessage) MessageTitle() string { - if c.messageTitle == nil { - s, _, _ := strings.Cut(strings.TrimSpace(c.MessageUTF8()), "\n") - c.messageTitle = new(strings.TrimSpace(s)) - } - return *c.messageTitle -} - -func (c *CommitMessage) MessageBody() string { - if c.messageBody == nil { - _, s, _ := strings.Cut(strings.TrimSpace(c.MessageUTF8()), "\n") - c.messageBody = new(strings.TrimSpace(s)) - } - return *c.messageBody -} - // ParentID returns oid of n-th parent (0-based index). // It returns nil if no such parent exists. func (c *Commit) ParentID(n int) (ObjectID, error) { diff --git a/modules/git/commit_message.go b/modules/git/commit_message.go new file mode 100644 index 00000000000..8fd3601f0d0 --- /dev/null +++ b/modules/git/commit_message.go @@ -0,0 +1,131 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "net/mail" + "regexp" + "strings" + "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" + +type CommitIdentity struct { + Name string + Email string +} + +// CommitMessageTrailerValues keys are all in lower-case +type CommitMessageTrailerValues map[string][]string + +type CommitMessage struct { + MessageRaw string + messageUTF8 *string + messageTitle *string + messageBody *string + + trailerValues CommitMessageTrailerValues + + allParticipants []*CommitIdentity +} + +func (c *CommitMessage) MessageUTF8() string { + if c.messageUTF8 == nil { + bs := charset.ToUTF8(util.UnsafeStringToBytes(c.MessageRaw), charset.ConvertOpts{ErrorReplacement: []byte{'?'}}) + c.messageUTF8 = new(util.UnsafeBytesToString(bs)) + } + return *c.messageUTF8 +} + +func (c *CommitMessage) MessageTitle() string { + if c.messageTitle == nil { + s, _, _ := strings.Cut(strings.TrimSpace(c.MessageUTF8()), "\n") + c.messageTitle = new(strings.TrimSpace(s)) + } + return *c.messageTitle +} + +func (c *CommitMessage) MessageBody() string { + if c.messageBody == nil { + _, s, _ := strings.Cut(strings.TrimSpace(c.MessageUTF8()), "\n") + c.messageBody = new(strings.TrimSpace(s)) + } + return *c.messageBody +} + +func (c *CommitMessage) MessageTrailer() CommitMessageTrailerValues { + if c.trailerValues == nil { + _, _, trailer := CommitMessageSplitTrailer(c.MessageUTF8()) + c.trailerValues = CommitMessageParseTrailer(trailer) + } + return c.trailerValues +} + +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?)*)$`) +}) + +func CommitMessageSplitTrailer(s string) (content, sep, trailer string) { + s = util.NormalizeStringEOL(s) + re := commitMessageTrailerSplit() + v := re.FindStringSubmatch(s) + if v == nil { + return s, "", "" + } + return v[re.SubexpIndex("content")], v[re.SubexpIndex("sep")], v[re.SubexpIndex("trailer")] +} + +func CommitMessageParseTrailer(s string) CommitMessageTrailerValues { + ret := CommitMessageTrailerValues{} + for line := range strings.SplitSeq(util.NormalizeStringEOL(s), "\n") { + k, v, ok := strings.Cut(line, ":") + if !ok { + continue + } + k, v = strings.TrimSpace(k), strings.TrimSpace(v) + kLower := strings.ToLower(k) + ret[kLower] = append(ret[kLower], v) + } + return ret +} + +// AllParticipantIdentities returns all the participants in the commit, the first one is the commit's author +func (c *Commit) AllParticipantIdentities() []*CommitIdentity { + if c.allParticipants != nil { + 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) { + if name == "" && email == "" { + return + } + emailLower := strings.ToLower(email) + if emailLower != "" && exclude.Contains(emailLower) { + return + } + c.allParticipants = append(c.allParticipants, &CommitIdentity{Name: name, Email: email}) + exclude.Add(emailLower) + } + addParticipant(c.Committer.Name, c.Committer.Email) + for _, coAuthorValue := range c.MessageTrailer()["co-authored-by"] { + addr, err := mail.ParseAddress(coAuthorValue) + if err == nil { + addParticipant(addr.Name, addr.Address) + } else { + addParticipant(coAuthorValue, "") + } + } + return c.allParticipants +} diff --git a/modules/git/commit_message_test.go b/modules/git/commit_message_test.go new file mode 100644 index 00000000000..049f1c03f78 --- /dev/null +++ b/modules/git/commit_message_test.go @@ -0,0 +1,80 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommitMessageSanitizesInvalidUTF8(t *testing.T) { + commit := &Commit{ + CommitMessage: CommitMessage{MessageRaw: "title \xff\n\n\n\nbody \xff\n\n\n"}, + } + assert.Equal(t, "title ÿ", commit.MessageTitle()) + assert.Equal(t, "body ÿ", commit.MessageBody()) + assert.Equal(t, "title ÿ\n\n\n\nbody ÿ\n\n\n", commit.MessageUTF8()) +} + +func TestCommitMessageTrailer(t *testing.T) { + cases := []struct { + msg, body, sep, trailer string + }{ + {"", "", "", ""}, + {"a", "a", "", ""}, + {"a\n\nk", "a\n\nk", "", ""}, + {"a\n\nk:v", "a", "\n\n", "k:v"}, + {"a\n--\nk:v", "a\n--\nk:v", "", ""}, + {"a\n---\nk:v", "a", "\n---\n", "k:v"}, + + {"k: v", "", "", "k: v"}, + {"\nk:v", "", "\n", "k:v"}, + {"\n\nk:v", "", "\n\n", "k:v"}, + + {"---\nk:v", "", "---\n", "k:v"}, + {"\n---\nk:v", "", "\n---\n", "k:v"}, + {"a:b\n---\nk:v", "a:b", "\n---\n", "k:v"}, + } + for _, c := range cases { + body, sep, trailer := CommitMessageSplitTrailer(c.msg) + assert.Equal(t, c.body, body, "input=%q", c.msg) + assert.Equal(t, c.sep, sep, "input=%q", c.msg) + assert.Equal(t, c.trailer, trailer, "input=%q", c.msg) + } +} + +func TestCommitMessageAllParticipantIdentities(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")}, + }, + } + for _, c := range cases { + assert.Equal(t, c.participant, c.commit.AllParticipantIdentities()) + } +} diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index a7668e4debe..5e3d2fba71b 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -159,15 +159,6 @@ ISO-8859-1`, commitFromReader.Signature.Payload) assert.Equal(t, commitFromReader, commitFromReader2) } -func TestCommitMessageSanitizesInvalidUTF8(t *testing.T) { - commit := &Commit{ - CommitMessage: CommitMessage{MessageRaw: "title \xff\n\n\n\nbody \xff\n\n\n"}, - } - assert.Equal(t, "title ÿ", commit.MessageTitle()) - assert.Equal(t, "body ÿ", commit.MessageBody()) - assert.Equal(t, "title ÿ\n\n\n\nbody ÿ\n\n\n", commit.MessageUTF8()) -} - func TestHasPreviousCommit(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") diff --git a/modules/git/config.go b/modules/git/config.go index b1ca18c9882..e6a2ac88173 100644 --- a/modules/git/config.go +++ b/modules/git/config.go @@ -46,10 +46,8 @@ func syncGitConfig(ctx context.Context) (err error) { return err } - if DefaultFeatures().CheckVersionAtLeast("2.10") { - if err := configSet(ctx, "receive.advertisePushOptions", "true"); err != nil { - return err - } + if err := configSet(ctx, "receive.advertisePushOptions", "true"); err != nil { + return err } if DefaultFeatures().CheckVersionAtLeast("2.18") { diff --git a/modules/git/git.go b/modules/git/git.go index 5359781968f..0c2deb0281e 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -22,7 +22,7 @@ import ( "github.com/hashicorp/go-version" ) -const RequiredVersion = "2.6.0" // the minimum Git version required +const RequiredVersion = "2.13.0" // the minimum Git version required type Features struct { gitVersion *version.Version @@ -173,13 +173,6 @@ func InitFull() (err error) { if err = InitSimple(); err != nil { return err } - - if setting.LFS.StartServer { - if !DefaultFeatures().CheckVersionAtLeast("2.1.2") { - return errors.New("LFS server support requires Git >= 2.1.2") - } - } - return syncGitConfig(context.Background()) } diff --git a/modules/git/gitcmd/error.go b/modules/git/gitcmd/error.go index 436f4e18ae4..8d5ec5f5e69 100644 --- a/modules/git/gitcmd/error.go +++ b/modules/git/gitcmd/error.go @@ -9,6 +9,8 @@ import ( "fmt" "os/exec" "strings" + + "gitea.dev/modules/util" ) type RunStdError interface { @@ -41,32 +43,14 @@ func (r *runStdError) Stderr() string { } func ErrorAsStderr(err error) (string, bool) { - var runErr RunStdError - if errors.As(err, &runErr) { + if runErr, ok := errors.AsType[RunStdError](err); ok { return runErr.Stderr(), true } return "", false } -func StderrHasPrefix(err error, prefix string) bool { - stderr, ok := ErrorAsStderr(err) - if !ok { - return false - } - return strings.HasPrefix(stderr, prefix) -} - -func StderrContains(err error, sub string) bool { - stderr, ok := ErrorAsStderr(err) - if !ok { - return false - } - return strings.Contains(stderr, sub) -} - func IsErrorExitCode(err error, code int) bool { - var exitError *exec.ExitError - if errors.As(err, &exitError) { + if exitError, ok := errors.AsType[*exec.ExitError](err); ok { return exitError.ExitCode() == code } return false @@ -85,11 +69,41 @@ func IsErrorCanceledOrKilled(err error) bool { return errors.Is(err, context.Canceled) || IsErrorSignalKilled(err) } -func IsStdErrorNotValidObjectName(err error) bool { +type StderrPrefix string + +type StderrSubStr string + +const ( + StderrNotValidObjectName StderrPrefix = "fatal: not a valid object name" + StderrNotTreeObject StderrPrefix = "fatal: not a tree object" + StderrPathSpec StderrPrefix = "fatal: pathspec" + StderrBadRevision StderrPrefix = "fatal: bad revision" + + StderrNoSuchRemote1 StderrPrefix = "fatal: no such remote" // git < 2.30, exit status 128 + StderrNoSuchRemote2 StderrPrefix = "error: no such remote" // git >= 2.30. exit status 2 + + // fatal: ambiguous argument 'origin': unknown revision or path not in the working tree. + StderrUnknownRevisionOrPath StderrSubStr = "unknown revision or path not in the working tree" +) + +func IsStderr[T StderrPrefix | StderrSubStr](err error, check T) bool { stderr, ok := ErrorAsStderr(err) - // Git is lowercasing the "fatal: Not a valid object name" error message - // ref: https://lore.kernel.org/git/pull.2052.git.1771836302101.gitgitgadget@gmail.com - return ok && strings.Contains(strings.ToLower(stderr), "fatal: not a valid object name") + if !ok { + return false + } + checkLen := len(check) + if len(stderr) < checkLen { + return false + } + switch any(check).(type) { + case StderrPrefix: + // Git is lowercasing the "fatal: Not a valid object name" error message + // ref: https://lore.kernel.org/git/pull.2052.git.1771836302101.gitgitgadget@gmail.com + return util.AsciiEqualFold(stderr[:checkLen], string(check)) + case StderrSubStr: + return strings.Contains(stderr, string(check)) + } + return false } type pipelineError struct { diff --git a/modules/git/remote.go b/modules/git/remote.go index f215cd36713..e94ca374e4d 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -15,13 +15,7 @@ import ( // GetRemoteAddress returns remote url of git repository in the repoPath with special remote name func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (string, error) { - var cmd *gitcmd.Command - if DefaultFeatures().CheckVersionAtLeast("2.7") { - cmd = gitcmd.NewCommand("remote", "get-url").AddDynamicArguments(remoteName) - } else { - cmd = gitcmd.NewCommand("config", "--get").AddDynamicArguments("remote." + remoteName + ".url") - } - + cmd := gitcmd.NewCommand("remote", "get-url").AddDynamicArguments(remoteName) result, _, err := cmd.WithDir(repoPath).RunStdString(ctx) if err != nil { return "", err @@ -72,11 +66,7 @@ func (err *ErrInvalidCloneAddr) Unwrap() error { // IsRemoteNotExistError checks the prefix of the error message to see whether a remote does not exist. func IsRemoteNotExistError(err error) bool { - // see: https://github.com/go-gitea/gitea/issues/32889#issuecomment-2571848216 - // Should not add space in the end, sometimes git will add a `:` - prefix1 := "fatal: No such remote" // git < 2.30, exit status 128 - prefix2 := "error: No such remote" // git >= 2.30. exit status 2 - return gitcmd.StderrHasPrefix(err, prefix1) || gitcmd.StderrHasPrefix(err, prefix2) + return gitcmd.IsStderr(err, gitcmd.StderrNoSuchRemote1) || gitcmd.IsStderr(err, gitcmd.StderrNoSuchRemote2) } // ParseRemoteAddr checks if given remote address is valid, diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index cbe5053346b..20827062df3 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -7,7 +7,6 @@ package git import ( "bytes" "io" - "os" "strconv" "strings" @@ -25,9 +24,9 @@ func (repo *Repository) GetTagCommitID(name string) (string, error) { return repo.GetRefCommitID(TagPrefix + name) } -// GetCommit returns commit object of by ID string. -func (repo *Repository) GetCommit(commitID string) (*Commit, error) { - id, err := repo.ConvertToGitID(commitID) +// GetCommit returns a commit object of by the git ref. +func (repo *Repository) GetCommit(ref string) (*Commit, error) { + id, err := repo.ConvertToGitID(ref) if err != nil { return nil, err } @@ -222,17 +221,20 @@ type CommitsByFileAndRangeOptions struct { Page int Since string Until string + + // when using FollowRename, there is no quick way to know the total count, so use hasMore to indicate if there are more commits to load + FollowRename bool } // CommitsByFileAndRange return the commits according revision file and the page -func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) { - gitCmd := gitcmd.NewCommand("rev-list"). - AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize). +func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) (commits []*Commit, hasMore bool, _ error) { + limit := setting.Git.CommitsRangeSize + gitCmd := gitcmd.NewCommand("--no-pager", "log"). + AddArguments("--pretty=tformat:%H"). + AddOptionFormat("--max-count=%d", limit+1). AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize) - gitCmd.AddDynamicArguments(opts.Revision) - - if opts.Not != "" { - gitCmd.AddOptionValues("--not", opts.Not) + if opts.FollowRename { + gitCmd.AddArguments("--follow") } if opts.Since != "" { gitCmd.AddOptionFormat("--since=%s", opts.Since) @@ -240,9 +242,12 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) if opts.Until != "" { gitCmd.AddOptionFormat("--until=%s", opts.Until) } + gitCmd.AddDynamicArguments(opts.Revision) + if opts.Not != "" { + gitCmd.AddOptionValues("--not", opts.Not) + } gitCmd.AddDashesAndList(opts.File) - var commits []*Commit stdoutReader, stdoutReaderClose := gitCmd.MakeStdoutPipe() defer stdoutReaderClose() err := gitCmd.WithDir(repo.Path). @@ -274,7 +279,12 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) } }). RunWithStderr(repo.Ctx) - return commits, err + + hasMore = len(commits) > limit + if hasMore { + commits = commits[:limit] + } + return commits, hasMore, err } // FilesCountBetween return the number of files changed between two commits @@ -398,7 +408,7 @@ func (repo *Repository) commitsBefore(id ObjectID, limit int) ([]*Commit, error) commits := make([]*Commit, 0, len(formattedLog)) for _, commit := range formattedLog { - branches, err := repo.getBranches(os.Environ(), commit.ID.String(), 2) + branches, err := repo.getBranches(nil, commit.ID.String(), 2) if err != nil { return nil, err } @@ -422,46 +432,17 @@ func (repo *Repository) getCommitsBeforeLimit(id ObjectID, num int) ([]*Commit, } func (repo *Repository) getBranches(env []string, commitID string, limit int) ([]string, error) { - if DefaultFeatures().CheckVersionAtLeast("2.7.0") { - stdout, _, err := gitcmd.NewCommand("for-each-ref", "--format=%(refname:strip=2)"). - AddOptionFormat("--count=%d", limit). - AddOptionValues("--contains", commitID, BranchPrefix). - WithDir(repo.Path). - WithEnv(env). - RunStdString(repo.Ctx) - if err != nil { - return nil, err - } - - branches := strings.Fields(stdout) - return branches, nil - } - - stdout, _, err := gitcmd.NewCommand("branch"). + stdout, _, err := gitcmd.NewCommand("for-each-ref", "--format=%(refname:strip=2)"). + AddOptionFormat("--count=%d", limit). AddOptionValues("--contains", commitID). - WithDir(repo.Path). + AddArguments(BranchPrefix). WithEnv(env). + WithDir(repo.Path). RunStdString(repo.Ctx) if err != nil { return nil, err } - - refs := strings.Split(stdout, "\n") - - var maxNum int - if len(refs) > limit { - maxNum = limit - } else { - maxNum = len(refs) - 1 - } - - branches := make([]string, maxNum) - for i, ref := range refs[:maxNum] { - parts := strings.Fields(ref) - - branches[i] = parts[len(parts)-1] - } - return branches, nil + return strings.Fields(stdout), nil } // GetCommitsFromIDs get commits from commit IDs @@ -504,16 +485,17 @@ func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID s parts := bytes.SplitSeq(bytes.TrimSpace(stdout), []byte{'\n'}) - // check the commits one by one until we find a commit contained by another branch + // check the commits one by one until we find a commit contained by another branch, // and we think this commit is the divergence point - for commitID := range parts { - branches, err := repo.getBranches(env, string(commitID), 2) + for part := range parts { + commitID := string(part) + branches, err := repo.getBranches(env, commitID, 2) if err != nil { return "", err } for _, b := range branches { if b != branch { - return string(commitID), nil + return commitID, nil } } } diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go index c90650532b8..d8b842beeed 100644 --- a/modules/git/repo_commit_nogogit.go +++ b/modules/git/repo_commit_nogogit.go @@ -109,16 +109,16 @@ func (repo *Repository) getCommitWithBatch(batch CatFileBatch, id ObjectID) (*Co } } -// ConvertToGitID returns a GitHash object from a potential ID string -func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { +// 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() if err != nil { return nil, err } - if len(commitID) == objectFormat.FullLength() && objectFormat.IsValid(commitID) { - ID, err := NewIDFromString(commitID) + if len(ref) == objectFormat.FullLength() && objectFormat.IsValid(ref) { + id, err := NewIDFromString(ref) if err == nil { - return ID, nil + return id, nil } } @@ -127,10 +127,10 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { return nil, err } defer cancel() - info, err := batch.QueryInfo(commitID) + info, err := batch.QueryInfo(ref) if err != nil { if IsErrNotExist(err) { - return nil, ErrNotExist{commitID, ""} + return nil, ErrNotExist{ref, ""} } return nil, err } diff --git a/modules/git/repo_commit_test.go b/modules/git/repo_commit_test.go index 517decf0ca3..fc905d24257 100644 --- a/modules/git/repo_commit_test.go +++ b/modules/git/repo_commit_test.go @@ -4,10 +4,11 @@ package git import ( - "os" "path/filepath" + "strings" "testing" + "gitea.dev/modules/git/gitcmd" "gitea.dev/modules/setting" "gitea.dev/modules/test" @@ -36,7 +37,7 @@ func TestRepository_GetCommitBranches(t *testing.T) { for _, testCase := range testCases { commit, err := bareRepo1.GetCommit(testCase.CommitID) assert.NoError(t, err) - branches, err := bareRepo1.getBranches(os.Environ(), commit.ID.String(), 2) + branches, err := bareRepo1.getBranches(nil, commit.ID.String(), 2) assert.NoError(t, err) assert.Equal(t, testCase.ExpectedBranches, branches) } @@ -140,11 +141,52 @@ func TestCommitsByFileAndRange(t *testing.T) { defer bareRepo1.Close() // "foo" has 3 commits in "master" branch - commits, err := bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 1}) + commits, hasMore, err := bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 1}) require.NoError(t, err) + assert.True(t, hasMore) assert.Len(t, commits, 2) - commits, err = bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 2}) + commits, hasMore, err = bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 2}) require.NoError(t, err) assert.Len(t, commits, 1) + assert.False(t, hasMore) + + repoFollowRenameDir := filepath.Join(t.TempDir(), "repo.git") + require.NoError(t, gitcmd.NewCommand("init").AddDynamicArguments(repoFollowRenameDir).Run(t.Context())) + _, _, runErr := gitcmd.NewCommand("fast-import").WithDir(repoFollowRenameDir).WithStdinBytes([]byte(strings.TrimSpace(` +blob +mark :1 +data 0 + +reset refs/heads/master +commit refs/heads/master +mark :2 +author Chi-Iroh 1778660718 +0200 +committer Chi-Iroh 1778660718 +0200 +data 10 +Add a.txt +M 100644 :1 a.txt + +commit refs/heads/master +mark :3 +author Chi-Iroh 1778660741 +0200 +committer Chi-Iroh 1778660741 +0200 +data 22 +Rename a.txt to b.txt +from :2 +D a.txt +M 100644 :1 b.txt + `))).RunStdString(t.Context()) + require.NoError(t, runErr) + + repoFollowRename, err := OpenRepository(t.Context(), repoFollowRenameDir) + require.NoError(t, err) + defer repoFollowRename.Close() + + commits, _, err = repoFollowRename.CommitsByFileAndRange(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}) + require.NoError(t, err) + assert.Len(t, commits, 2) } diff --git a/modules/git/tree_nogogit.go b/modules/git/tree_nogogit.go index 1d78ecdee24..ebb4c7bb122 100644 --- a/modules/git/tree_nogogit.go +++ b/modules/git/tree_nogogit.go @@ -7,7 +7,6 @@ package git import ( "io" - "strings" "gitea.dev/modules/git/gitcmd" ) @@ -65,7 +64,7 @@ func (t *Tree) ListEntries() (Entries, error) { stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithDir(t.repo.Path).RunStdBytes(t.repo.Ctx) if runErr != nil { - if gitcmd.IsStdErrorNotValidObjectName(runErr) || strings.Contains(runErr.Error(), "fatal: not a tree object") { + if gitcmd.IsStderr(runErr, gitcmd.StderrNotValidObjectName) || gitcmd.IsStderr(runErr, gitcmd.StderrNotTreeObject) { return nil, ErrNotExist{ ID: t.ID.String(), } diff --git a/modules/httpcache/httpcache.go b/modules/httpcache/httpcache.go index f55e13ce594..785363c29ca 100644 --- a/modules/httpcache/httpcache.go +++ b/modules/httpcache/httpcache.go @@ -15,9 +15,8 @@ import ( ) type CacheControlOptions struct { - IsPublic bool - MaxAge time.Duration - NoTransform bool + IsPublic bool + MaxAge time.Duration } // SetCacheControlInHeader sets suitable cache-control headers in the response @@ -38,25 +37,19 @@ func SetCacheControlInHeader(h http.Header, opts *CacheControlOptions) { directives = append(directives, "max-age=0", publicPrivate, "must-revalidate") h.Set("X-Gitea-Debug", fmt.Sprintf("RUN_MODE=%v, MaxAge=%s", setting.RunMode, opts.MaxAge)) } - - if opts.NoTransform { - directives = append(directives, "no-transform") - } h.Set("Cache-Control", strings.Join(directives, ", ")) } func CacheControlForPublicStatic() *CacheControlOptions { return &CacheControlOptions{ - IsPublic: true, - MaxAge: setting.StaticCacheTime, - NoTransform: true, + IsPublic: true, + MaxAge: setting.StaticCacheTime, } } func CacheControlForPrivateStatic() *CacheControlOptions { return &CacheControlOptions{ - MaxAge: setting.StaticCacheTime, - NoTransform: true, + MaxAge: setting.StaticCacheTime, } } diff --git a/modules/httpcache/httpcache_test.go b/modules/httpcache/httpcache_test.go index f9625981aaf..d715cac4a24 100644 --- a/modules/httpcache/httpcache_test.go +++ b/modules/httpcache/httpcache_test.go @@ -18,7 +18,7 @@ func TestHandleGenericETagCache(t *testing.T) { matchedEtag := `"matched-etag"` lastModifiedTime := new(time.Date(2021, time.January, 2, 15, 4, 5, 0, time.FixedZone("test-zone", 8*3600))) lastModified := lastModifiedTime.UTC().Format(http.TimeFormat) - cacheControl := "max-age=0, private, must-revalidate, no-transform" + cacheControl := "max-age=0, private, must-revalidate" type testCase struct { name string reqHeaders map[string]string diff --git a/modules/httplib/serve.go b/modules/httplib/serve.go index cf2e049cb7a..e2da9e85fa7 100644 --- a/modules/httplib/serve.go +++ b/modules/httplib/serve.go @@ -94,9 +94,8 @@ func ServeSetHeaders(w http.ResponseWriter, opts ServeHeaderOptions) { } httpcache.SetCacheControlInHeader(header, &httpcache.CacheControlOptions{ - IsPublic: opts.CacheIsPublic, - MaxAge: opts.CacheDuration, - NoTransform: true, + IsPublic: opts.CacheIsPublic, + MaxAge: opts.CacheDuration, }) if !opts.LastModified.IsZero() { diff --git a/modules/markup/html.go b/modules/markup/html.go index 4943bdf4a5f..a21c4707113 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -14,8 +14,10 @@ import ( "sync" "gitea.dev/modules/htmlutil" + "gitea.dev/modules/log" "gitea.dev/modules/markup/common" "gitea.dev/modules/translation" + "gitea.dev/modules/util" "golang.org/x/net/html" "golang.org/x/net/html/atom" @@ -151,8 +153,7 @@ func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) e } // PostProcessCommitMessage will use the same logic as PostProcess, but will disable the shortLinkProcessor. -// FIXME: this function and its family have a very strange design: it takes HTML as input and output, processes the "escaped" content. -func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) (template.HTML, error) { +func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) template.HTML { procs := []processor{ fullIssuePatternProcessor, comparePatternProcessor, @@ -166,8 +167,7 @@ func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) (templa emojiProcessor, emojiShortCodeProcessor, } - s, err := postProcessString(ctx, procs, string(content)) - return template.HTML(s), err + return postProcessHTML(ctx, procs, content) } var emojiProcessors = []processor{ @@ -189,7 +189,7 @@ func isBareURLSubject(content string) bool { // PostProcessCommitMessageSubject will use the same logic as PostProcess and // PostProcessCommitMessage, but will disable the shortLinkProcessor and // emailAddressProcessor, and wraps the whole subject in defaultLink. -func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content string) (string, error) { +func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink string, content template.HTML) template.HTML { procs := []processor{ fullIssuePatternProcessor, comparePatternProcessor, @@ -207,7 +207,7 @@ func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content st // plain text inside defaultLink. Partial URLs inside larger text still become // their own links (nested anchors aren't legal HTML, so the outer defaultLink // naturally breaks on that span, same as on GitHub). - if !isBareURLSubject(content) { + if !isBareURLSubject(string(content)) { procs = append(procs, linkProcessor) } procs = append(procs, func(ctx *RenderContext, node *html.Node) { @@ -215,27 +215,28 @@ func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content st node.Type = html.ElementNode node.Data = "a" node.DataAtom = atom.A - node.Attr = []html.Attribute{{Key: "href", Val: defaultLink}, {Key: "class", Val: "muted"}} + node.Attr = []html.Attribute{{Key: "href", Val: defaultLink}, {Key: "class", Val: "muted title-full-link"}} node.FirstChild, node.LastChild = ch, ch }) - return postProcessString(ctx, procs, content) + rendered := postProcessHTML(ctx, procs, content) + return htmlutil.HTMLFormat(`%s`, rendered) } // PostProcessIssueTitle to process title on individual issue/pull page -func PostProcessIssueTitle(ctx *RenderContext, title string) (string, error) { - return postProcessString(ctx, []processor{ +func PostProcessIssueTitle(ctx *RenderContext, titleHTML template.HTML) template.HTML { + return postProcessHTML(ctx, []processor{ issueIndexPatternProcessor, commitCrossReferencePatternProcessor, hashCurrentPatternProcessor, emojiShortCodeProcessor, emojiProcessor, - }, title) + }, titleHTML) } // PostProcessDescriptionHTML will use similar logic as PostProcess, but will // use a single special linkProcessor. -func PostProcessDescriptionHTML(ctx *RenderContext, content string) (string, error) { - return postProcessString(ctx, []processor{ +func PostProcessDescriptionHTML(ctx *RenderContext, content template.HTML) template.HTML { + return postProcessHTML(ctx, []processor{ descriptionLinkProcessor, emojiShortCodeProcessor, emojiProcessor, @@ -243,17 +244,18 @@ func PostProcessDescriptionHTML(ctx *RenderContext, content string) (string, err } // PostProcessEmoji for when we want to just process emoji and shortcodes -// in various places it isn't already run through the normal markdown processor -func PostProcessEmoji(ctx *RenderContext, content string) (string, error) { - return postProcessString(ctx, emojiProcessors, content) +// in various places it isn't already run through the normal Markdown processor +func PostProcessEmoji(ctx *RenderContext, content template.HTML) template.HTML { + return postProcessHTML(ctx, emojiProcessors, content) } -func postProcessString(ctx *RenderContext, procs []processor, content string) (string, error) { +func postProcessHTML(ctx *RenderContext, procs []processor, content template.HTML) template.HTML { var buf strings.Builder - if err := postProcess(ctx, procs, strings.NewReader(content), &buf); err != nil { - return "", err + if err := postProcess(ctx, procs, strings.NewReader(string(content)), &buf); err != nil { + log.Warn("postProcessHTML err: %v, input: %s", err, util.TruncateRunes(string(content), 200)) + return content } - return buf.String(), nil + return template.HTML(buf.String()) } func RenderTocHeadingItems(ctx *RenderContext, nodeDetailsAttrs map[string]string, out io.Writer) { diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go index 186489ae722..6036f368901 100644 --- a/modules/markup/html_internal_test.go +++ b/modules/markup/html_internal_test.go @@ -5,6 +5,7 @@ package markup import ( "fmt" + "html/template" "strconv" "strings" "testing" @@ -260,9 +261,8 @@ func TestRender_PostProcessIssueTitle(t *testing.T) { "repo": "someRepo", "style": IssueNameStyleNumeric, } - actual, err := PostProcessIssueTitle(NewTestRenderContext(metas), "#1") - assert.NoError(t, err) - assert.Equal(t, "#1", actual) + actual := PostProcessIssueTitle(NewTestRenderContext(metas), "#1") + assert.Equal(t, template.HTML("#1"), actual) } func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) { diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go index f29f8837349..fae9d06e2a9 100644 --- a/modules/markup/markdown/ast.go +++ b/modules/markup/markdown/ast.go @@ -95,13 +95,13 @@ type Icon struct { // ColorPreview is an inline for a color preview type ColorPreview struct { ast.BaseInline - Color []byte + Color string } // Dump implements Node.Dump. func (n *ColorPreview) Dump(source []byte, level int) { m := map[string]string{} - m["Color"] = string(n.Color) + m["Color"] = n.Color ast.DumpHelper(n, source, level, m, nil) } @@ -114,7 +114,7 @@ func (n *ColorPreview) Kind() ast.NodeKind { } // NewColorPreview returns a new Span node. -func NewColorPreview(color []byte) *ColorPreview { +func NewColorPreview(color string) *ColorPreview { return &ColorPreview{ BaseInline: ast.BaseInline{}, Color: color, @@ -170,3 +170,14 @@ func (n *RawHTML) Kind() ast.NodeKind { func NewRawHTML(rawHTML template.HTML) *RawHTML { return &RawHTML{rawHTML: rawHTML} } + +func childSingleText(node ast.Node, source []byte) (string, bool) { + if node.FirstChild() == nil || node.FirstChild() != node.LastChild() { + return "", false + } + c, ok := node.FirstChild().(*ast.Text) + if !ok { + return "", false + } + return string(c.Segment.Value(source)), true +} diff --git a/modules/markup/markdown/transform_blockquote.go b/modules/markup/markdown/transform_blockquote.go index b6cbef9a0a8..2ec7cec9d5e 100644 --- a/modules/markup/markdown/transform_blockquote.go +++ b/modules/markup/markdown/transform_blockquote.go @@ -46,7 +46,10 @@ func (g *ASTTransformer) extractBlockquoteAttentionEmphasis(firstParagraph ast.N if !ok { return "", nil } - val1 := string(node1.Text(reader.Source())) //nolint:staticcheck // Text is deprecated + val1, ok := childSingleText(node1, reader.Source()) + if !ok { + return "", nil + } attentionType := strings.ToLower(val1) if g.attentionTypes.Contains(attentionType) { return attentionType, []ast.Node{node1} diff --git a/modules/markup/markdown/transform_codespan.go b/modules/markup/markdown/transform_codespan.go index 900c38b129e..df4a56fd9e5 100644 --- a/modules/markup/markdown/transform_codespan.go +++ b/modules/markup/markdown/transform_codespan.go @@ -39,7 +39,7 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod r.Writer.RawWrite(w, value) } case *ColorPreview: - _ = r.renderInternal.FormatWithSafeAttrs(w, ``, string(v.Color)) + _ = r.renderInternal.FormatWithSafeAttrs(w, ``, v.Color) } } return ast.WalkSkipChildren, nil @@ -68,8 +68,11 @@ func cssColorHandler(value string) bool { } func (g *ASTTransformer) transformCodeSpan(_ *markup.RenderContext, v *ast.CodeSpan, reader text.Reader) { - colorContent := v.Text(reader.Source()) //nolint:staticcheck // Text is deprecated - if cssColorHandler(string(colorContent)) { + colorContent, ok := childSingleText(v, reader.Source()) + if !ok { + return + } + if cssColorHandler(colorContent) { v.AppendChild(v, NewColorPreview(colorContent)) } } diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go index e38852a3d5a..9c7259dc8cc 100644 --- a/modules/markup/sanitizer_default.go +++ b/modules/markup/sanitizer_default.go @@ -63,6 +63,38 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("loading").OnElements("img") + // MathML Core (https://www.w3.org/TR/mathml-core/) + mathMLElements := []string{ + "math", + // token elements + "mi", "mn", "mo", "mtext", "mspace", "ms", + // layout elements + "mrow", "mfrac", "msqrt", "mroot", "mstyle", "merror", "mpadded", "mphantom", + // scripting elements + "msub", "msup", "msubsup", "munder", "mover", "munderover", "mmultiscripts", "mprescripts", "none", + // tabular elements + "mtable", "mtr", "mtd", + // semantic annotations + "semantics", "annotation", "annotation-xml", + } + policy.AllowAttrs("display", "alttext").OnElements("math") + policy.AllowAttrs( + // global presentation attributes + "dir", "displaystyle", "mathbackground", "mathcolor", "mathsize", "mathvariant", "scriptlevel", + // operator attributes + "accent", "accentunder", "fence", "form", "largeop", "lspace", "maxsize", "minsize", "movablelimits", "rspace", "separator", "stretchy", "symmetric", + // space and padding attributes + "depth", "height", "voffset", "width", + // fraction attribute + "linethickness", + // table attributes + "columnalign", "columnlines", "columnspacing", "frame", "framespacing", "rowalign", "rowlines", "rowspacing", + // cell attributes + "columnspan", + // annotation attribute + "encoding", + ).OnElements(mathMLElements...) + // Allow generally safe attributes (reference: https://github.com/jch/html-pipeline) generalSafeAttrs := []string{ "abbr", "accept", "accept-charset", diff --git a/modules/markup/sanitizer_default_test.go b/modules/markup/sanitizer_default_test.go index e66f00c02f6..e344a96722f 100644 --- a/modules/markup/sanitizer_default_test.go +++ b/modules/markup/sanitizer_default_test.go @@ -61,6 +61,9 @@ func TestSanitizer(t *testing.T) { // picture `c`, `c`, + // MathML + ``, ``, + // Disallow dangerous url schemes `bad`, `bad`, `bad`, `bad`, @@ -72,6 +75,6 @@ func TestSanitizer(t *testing.T) { } for i := 0; i < len(testCases); i += 2 { - assert.Equal(t, testCases[i+1], string(Sanitize(testCases[i]))) + assert.Equal(t, testCases[i+1], string(Sanitize(testCases[i])), "input: %s", testCases[i]) } } diff --git a/modules/paginator/paginator.go b/modules/paginator/paginator.go index 942bf7117b6..ee06d70dca5 100644 --- a/modules/paginator/paginator.go +++ b/modules/paginator/paginator.go @@ -37,10 +37,11 @@ type Paginator struct { total int // total rows count, -1 means unknown totalPages int // total pages count, -1 means unknown current int // current page number - curRows int // current page rows count pagingNum int // how many rows in one page numPages int // how many pages to show on the UI + + hasNext *bool // used for total=-1 ("unlimited paging") } // New initialize a new pagination calculation and returns a Paginator as result. @@ -60,15 +61,13 @@ func New(total, pagingNum, current, numPages int) *Paginator { } } -func (p *Paginator) SetCurRows(rows int) { +func (p *Paginator) SetUnlimitedPaging(curRows int, hasNext bool) { // For "unlimited paging", we need to know the rows of current page to determine if there is a next page. - // There is still an edge case: when curRows==pagingNum, then the "next page" will be an empty page. - // Ideally we should query one more row to determine if there is really a next page, but it's impossible in current framework. - p.curRows = rows - if p.total == -1 && p.current == 1 && !p.HasNext() { + p.hasNext = &hasNext + if p.total == -1 && p.current == 1 && !hasNext { // if there is only one page for the "unlimited paging", set total rows/pages count // then the tmpl could decide to hide the nav bar. - p.total = rows + p.total = curRows p.totalPages = util.Iif(p.total == 0, 0, 1) } } @@ -92,8 +91,8 @@ func (p *Paginator) Previous() int { // HasNext returns true if there is a next page relative to current page. func (p *Paginator) HasNext() bool { - if p.total == -1 { - return p.curRows >= p.pagingNum + if p.hasNext != nil { + return *p.hasNext } return p.current*p.pagingNum < p.total } diff --git a/modules/repository/commits.go b/modules/repository/commits.go index d40af3d82c4..318b052ef18 100644 --- a/modules/repository/commits.go +++ b/modules/repository/commits.go @@ -9,19 +9,15 @@ import ( "net/url" "time" - "gitea.dev/models/avatars" repo_model "gitea.dev/models/repo" user_model "gitea.dev/models/user" - "gitea.dev/modules/cache" - "gitea.dev/modules/cachegroup" "gitea.dev/modules/git" "gitea.dev/modules/gitrepo" - "gitea.dev/modules/log" - "gitea.dev/modules/setting" api "gitea.dev/modules/structs" ) // PushCommit represents a commit in a push operation. +// This struct is marshaled as JSON (see ActionContent2Commits) type PushCommit struct { Sha1 string Message string @@ -33,6 +29,7 @@ type PushCommit struct { } // PushCommits represents list of commits in a push operation. +// This struct is marshaled as JSON (see ActionContent2Commits) type PushCommits struct { Commits []*PushCommit HeadCommit *PushCommit @@ -128,26 +125,6 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repo *repo_model return commits, headCommit, nil } -// AvatarLink tries to match user in database with e-mail -// in order to show custom avatar, and falls back to general avatar link. -func (pc *PushCommits) AvatarLink(ctx context.Context, email string) string { - size := avatars.DefaultAvatarPixelSize * setting.Avatar.RenderedSizeFactor - - v, _ := cache.GetWithContextCache(ctx, cachegroup.EmailAvatarLink, email, func(ctx context.Context, email string) (string, error) { - u, err := user_model.GetUserByEmail(ctx, email) - if err != nil { - if !user_model.IsErrUserNotExist(err) { - log.Error("GetUserByEmail: %v", err) - return "", err - } - return avatars.GenerateEmailAvatarFastLink(ctx, email, size), nil - } - return u.AvatarLinkWithSize(ctx, size), nil - }) - - return v -} - // CommitToPushCommit transforms a git.Commit to PushCommit type. func CommitToPushCommit(commit *git.Commit) *PushCommit { return &PushCommit{ diff --git a/modules/repository/commits_test.go b/modules/repository/commits_test.go index c0f00337b92..5e6266d9f21 100644 --- a/modules/repository/commits_test.go +++ b/modules/repository/commits_test.go @@ -4,14 +4,12 @@ package repository import ( - "strconv" "testing" "time" repo_model "gitea.dev/models/repo" "gitea.dev/models/unittest" "gitea.dev/modules/git" - "gitea.dev/modules/setting" "github.com/stretchr/testify/assert" ) @@ -99,38 +97,6 @@ func TestPushCommits_ToAPIPayloadCommits(t *testing.T) { assert.Equal(t, []string{"readme.md"}, headCommit.Modified) } -func TestPushCommits_AvatarLink(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - pushCommits := NewPushCommits() - pushCommits.Commits = []*PushCommit{ - { - Sha1: "abcdef1", - CommitterEmail: "user2@example.com", - CommitterName: "User Two", - AuthorEmail: "user4@example.com", - AuthorName: "User Four", - Message: "message1", - }, - { - Sha1: "abcdef2", - CommitterEmail: "user2@example.com", - CommitterName: "User Two", - AuthorEmail: "user2@example.com", - AuthorName: "User Two", - Message: "message2", - }, - } - - assert.Equal(t, - "/avatars/ab53a2911ddf9b4817ac01ddcd3d975f?size="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor), - pushCommits.AvatarLink(t.Context(), "user2@example.com")) - - assert.Equal(t, - "/assets/img/avatar_default.png", - pushCommits.AvatarLink(t.Context(), "nonexistent@example.com")) -} - func TestCommitToPushCommit(t *testing.T) { now := time.Now() sig := &git.Signature{ diff --git a/modules/setting/config/value.go b/modules/setting/config/value.go index 655120c1804..240f48243cc 100644 --- a/modules/setting/config/value.go +++ b/modules/setting/config/value.go @@ -78,11 +78,16 @@ func isZeroOrEmpty(v any) bool { return false } +var SkipDatabaseConfig bool + func (opt *Option[T]) ValueRevision(ctx context.Context) (v T, rev int, has bool) { dg := GetDynGetter() if dg == nil { // this is an edge case: the database is not initialized but the system setting is going to be used // it should panic to avoid inconsistent config values (from config / system setting) and fix the code + if SkipDatabaseConfig { + return opt.DefaultValue(), 0, false + } panic("no config dyn value getter") } diff --git a/modules/setting/ssh.go b/modules/setting/ssh.go index 948ce773c52..683c90f2243 100644 --- a/modules/setting/ssh.go +++ b/modules/setting/ssh.go @@ -9,6 +9,7 @@ import ( "text/template" "time" + "gitea.dev/modules/consts" "gitea.dev/modules/log" "gitea.dev/modules/util" @@ -52,8 +53,8 @@ var SSH = struct { Domain: "", Port: 22, MinimumKeySizeCheck: true, - MinimumKeySizes: map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 3071}, - ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gogs.rsa"}, + MinimumKeySizes: map[string]int{"ed25519": consts.AsymKeyMinBitsEC, "ed25519-sk": consts.AsymKeyMinBitsEC, "ecdsa": consts.AsymKeyMinBitsEC, "ecdsa-sk": consts.AsymKeyMinBitsEC, "rsa": consts.AsymKeyMinBitsRsa}, + ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gitea.ed25519", "ssh/gitea.ecdsa", "ssh/gogs.rsa"}, AuthorizedKeysCommandTemplate: "{{.AppPath}} --config={{.CustomConf}} serv key-{{.Key.ID}}", PerWriteTimeout: PerWriteTimeout, PerWritePerKbTimeout: PerWritePerKbTimeout, diff --git a/modules/ssh/ssh.go b/modules/ssh/ssh.go index 94e29969b02..78e4b0805b3 100644 --- a/modules/ssh/ssh.go +++ b/modules/ssh/ssh.go @@ -6,9 +6,6 @@ package ssh import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" "encoding/pem" "errors" "io" @@ -23,11 +20,11 @@ import ( "syscall" asymkey_model "gitea.dev/models/asymkey" + "gitea.dev/modules/generate" "gitea.dev/modules/graceful" "gitea.dev/modules/log" "gitea.dev/modules/process" "gitea.dev/modules/setting" - "gitea.dev/modules/util" "github.com/gliderlabs/ssh" gossh "golang.org/x/crypto/ssh" @@ -59,7 +56,7 @@ func getExitStatusFromError(err error) int { return 0 } - exitErr, ok := err.(*exec.ExitError) + exitErr, ok := errors.AsType[*exec.ExitError](err) if !ok { return 1 } @@ -322,7 +319,7 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { } // sshConnectionFailed logs a failed connection -// - this mainly exists to give a nice function name in logging +// - this mainly exists to give a nice function name in logging func sshConnectionFailed(conn net.Conn, err error) { // Log the underlying error with a specific message log.Warn("Failed connection from %s with error: %v", conn.RemoteAddr(), err) @@ -351,40 +348,37 @@ func Listen(host string, port int, ciphers, keyExchanges, macs []string) { }, } - keys := make([]string, 0, len(setting.SSH.ServerHostKeys)) + hostKeyFiles := make([]string, 0, len(setting.SSH.ServerHostKeys)) for _, key := range setting.SSH.ServerHostKeys { - isExist, err := util.IsExist(key) + _, err := os.Stat(key) if err != nil { - log.Fatal("Unable to check if %s exists. Error: %v", setting.SSH.ServerHostKeys, err) - } - if isExist { - keys = append(keys, key) + if !errors.Is(err, os.ErrNotExist) { + log.Fatal("Unable to check if %s exists. Error: %v", setting.SSH.ServerHostKeys, err) + } + continue } + hostKeyFiles = append(hostKeyFiles, key) } - if len(keys) == 0 { - filePath := filepath.Dir(setting.SSH.ServerHostKeys[0]) - - if err := os.MkdirAll(filePath, os.ModePerm); err != nil { - log.Error("Failed to create dir %s: %v", filePath, err) + if len(hostKeyFiles) == 0 { + hostKeyDir := filepath.Dir(setting.SSH.ServerHostKeys[0]) + err := os.MkdirAll(hostKeyDir, os.ModePerm) + if err != nil { + log.Error("Failed to create dir %s: %v", hostKeyDir, err) } - - err := GenKeyPair(setting.SSH.ServerHostKeys[0]) + hostKeyFiles, err = InitDefaultHostKeys(hostKeyDir) if err != nil { log.Fatal("Failed to generate private key: %v", err) } - log.Trace("New private key is generated: %s", setting.SSH.ServerHostKeys[0]) - keys = append(keys, setting.SSH.ServerHostKeys[0]) } - for _, key := range keys { - log.Info("Adding SSH host key: %s", key) - err := srv.SetOption(ssh.HostKeyFile(key)) + for _, keyFile := range hostKeyFiles { + log.Info("Adding SSH host key: %s", keyFile) + err := srv.SetOption(ssh.HostKeyFile(keyFile)) if err != nil { log.Error("Failed to set Host Key. %s", err) } } - go func() { _, _, finished := process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), "Service: Built-in SSH server", process.SystemProcessType, true) defer finished() @@ -395,43 +389,44 @@ func Listen(host string, port int, ciphers, keyExchanges, macs []string) { // GenKeyPair make a pair of public and private keys for SSH access. // Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file. // Private Key generated is PEM encoded -func GenKeyPair(keyPath string) error { - privateKey, err := rsa.GenerateKey(rand.Reader, 4096) +func GenKeyPair(keyPath string, keyType generate.SSHKeyType, bits int) error { + publicKey, privateKeyPEM, err := generate.NewSSHKey(keyType, bits) if err != nil { return err } - privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)} - f, err := os.OpenFile(keyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) - if err != nil { - return err - } - defer func() { - if err = f.Close(); err != nil { - log.Error("Close: %v", err) - } - }() - - if err := pem.Encode(f, privateKeyPEM); err != nil { - return err - } - - // generate public key - pub, err := gossh.NewPublicKey(&privateKey.PublicKey) + public := gossh.MarshalAuthorizedKey(publicKey) + privateKeyBuf := &bytes.Buffer{} + err = pem.Encode(privateKeyBuf, privateKeyPEM) if err != nil { return err } - public := gossh.MarshalAuthorizedKey(pub) - p, err := os.OpenFile(keyPath+".pub", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) + err = os.WriteFile(keyPath, privateKeyBuf.Bytes(), 0o600) if err != nil { return err } - defer func() { - if err = p.Close(); err != nil { - log.Error("Close: %v", err) - } - }() - _, err = p.Write(public) - return err + + return os.WriteFile(keyPath+".pub", public, 0o644) +} + +// InitDefaultHostKeys mirrors how ssh-keygen -A operates +// it runs checks if public and private keys are already defined and creates new ones if not present +// key naming does not follow the OpenSSH convention due to existing settings being gitea.{KeyType} so generation follows gitea convention +func InitDefaultHostKeys(path string) (keyFiles []string, _ error) { + var errs []error + keyTypes := []generate.SSHKeyType{generate.SSHKeyRSA, generate.SSHKeyECDSA, generate.SSHKeyED25519} + for _, keyType := range keyTypes { + keyPath := filepath.Join(path, "gitea."+string(keyType)) + _, errStatPriv := os.Stat(keyPath) + if errStatPriv != nil { + err := GenKeyPair(keyPath, keyType, 0) + if err != nil { + errs = append(errs, err) + continue + } + } + keyFiles = append(keyFiles, keyPath) + } + return keyFiles, errors.Join(errs...) } diff --git a/modules/ssh/ssh_test.go b/modules/ssh/ssh_test.go new file mode 100644 index 00000000000..ad9ac813d4c --- /dev/null +++ b/modules/ssh/ssh_test.go @@ -0,0 +1,123 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package ssh + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "os" + "path/filepath" + "testing" + + "gitea.dev/modules/generate" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + gossh "golang.org/x/crypto/ssh" +) + +func TestGenKeyPair(t *testing.T) { + testCases := []struct { + keyType generate.SSHKeyType + expectedType any + }{ + { + keyType: generate.SSHKeyRSA, + expectedType: &rsa.PrivateKey{}, + }, + { + keyType: generate.SSHKeyED25519, + expectedType: &ed25519.PrivateKey{}, + }, + { + keyType: generate.SSHKeyECDSA, + expectedType: &ecdsa.PrivateKey{}, + }, + } + tmpDir := t.TempDir() + for _, tc := range testCases { + name := "gitea." + string(tc.keyType) + fn := filepath.Join(tmpDir, name) + t.Run("Generate "+name, func(t *testing.T) { + require.NoError(t, GenKeyPair(fn, tc.keyType, 0)) + + bytes, err := os.ReadFile(fn) + require.NoError(t, err) + + privateKey, err := gossh.ParseRawPrivateKey(bytes) + require.NoError(t, err) + assert.IsType(t, tc.expectedType, privateKey) + }) + } + t.Run("Generate unknown key type", func(t *testing.T) { + err := GenKeyPair(t.TempDir()+"gitea.badkey", "badkey", 0) + require.Error(t, err) + }) +} + +func TestInitKeys(t *testing.T) { + tempDir := t.TempDir() + + keyTypes := []string{"rsa", "ecdsa", "ed25519"} + for _, keyType := range keyTypes { + privKeyPath := filepath.Join(tempDir, "gitea."+keyType) + pubKeyPath := filepath.Join(tempDir, "gitea."+keyType+".pub") + assert.NoFileExists(t, privKeyPath) + assert.NoFileExists(t, pubKeyPath) + } + + // Test basic creation + keyFiles, err := InitDefaultHostKeys(tempDir) + require.NoError(t, err) + assert.Len(t, keyFiles, len(keyTypes)) + + metadata := map[string]os.FileInfo{} + for _, keyType := range keyTypes { + privKeyPath := filepath.Join(tempDir, "gitea."+keyType) + pubKeyPath := filepath.Join(tempDir, "gitea."+keyType+".pub") + info, err := os.Stat(privKeyPath) + require.NoError(t, err) + metadata[privKeyPath] = info + + info, err = os.Stat(pubKeyPath) + require.NoError(t, err) + metadata[pubKeyPath] = info + } + + // Test recreation on missing private key and noop for missing pub key + require.NoError(t, os.Remove(filepath.Join(tempDir, "gitea.ecdsa.pub"))) + require.NoError(t, os.Remove(filepath.Join(tempDir, "gitea.ed25519"))) + + keyFiles, err = InitDefaultHostKeys(tempDir) + require.NoError(t, err) + assert.Len(t, keyFiles, len(keyTypes)) + + for _, keyType := range keyTypes { + privKeyPath := filepath.Join(tempDir, "gitea."+keyType) + pubKeyPath := filepath.Join(tempDir, "gitea."+keyType+".pub") + + infoPriv, err := os.Stat(privKeyPath) + require.NoError(t, err) + + switch keyType { + case "rsa": + // No modification to RSA key + infoPub, err := os.Stat(pubKeyPath) + require.NoError(t, err) + assert.Equal(t, metadata[privKeyPath], infoPriv) + assert.Equal(t, metadata[pubKeyPath], infoPub) + case "ecdsa": + // ECDSA public key should be missing, private unchanged + assert.Equal(t, metadata[privKeyPath], infoPriv) + assert.NoFileExists(t, pubKeyPath) + case "ed25519": + // ed25519 private key was removed, so both keys regenerated + infoPub, err := os.Stat(pubKeyPath) + require.NoError(t, err) + assert.NotEqual(t, metadata[privKeyPath], infoPriv) + assert.NotEqual(t, metadata[pubKeyPath], infoPub) + } + } +} diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 3c7f9b0a7a1..46f90be78ad 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -10,9 +10,10 @@ import ( "math" "net/url" "regexp" + "slices" "strings" - "unicode" + user_model "gitea.dev/models/gituser" issues_model "gitea.dev/models/issues" "gitea.dev/models/renderhelper" "gitea.dev/models/repo" @@ -23,6 +24,7 @@ import ( "gitea.dev/modules/log" "gitea.dev/modules/markup" "gitea.dev/modules/markup/markdown" + "gitea.dev/modules/repository" "gitea.dev/modules/reqctx" "gitea.dev/modules/setting" "gitea.dev/modules/svg" @@ -32,67 +34,43 @@ import ( ) type RenderUtils struct { - ctx reqctx.RequestContext + ctx reqctx.RequestContext + avatarUtils *AvatarUtils } func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils { - return &RenderUtils{ctx: ctx} + return &RenderUtils{ctx: ctx, avatarUtils: NewAvatarUtils(ctx)} } -// RenderCommitMessage renders commit message with XSS-safe and special links. +// RenderCommitMessage renders commit message title (only title) func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) template.HTML { - cleanMsg := template.HTML(template.HTMLEscapeString(msg)) - // we can safely assume that it will not return any error, since there shouldn't be any special HTML. - // "repo" can be nil when rendering commit messages for deleted repositories in a user's dashboard feed. - fullMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), cleanMsg) - if err != nil { - log.Error("PostProcessCommitMessage: %v", err) - return "" - } - msgLines := strings.Split(strings.TrimSpace(string(fullMessage)), "\n") - if len(msgLines) == 0 { - return "" - } - return renderCodeBlock(template.HTML(msgLines[0])) + msgLine := strings.TrimSpace(msg) + msgLine, _, _ = strings.Cut(msgLine, "\n") + msgLine = strings.TrimSpace(msgLine) + rendered := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), htmlutil.EscapeString(msgLine)) + return renderCodeBlock(rendered) } // RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to // the provided default url, handling for special links without email to links. func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, repo *repo.Repository) template.HTML { - msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace) - lineEnd := strings.IndexByte(msgLine, '\n') - if lineEnd > 0 { - msgLine = msgLine[:lineEnd] - } - msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace) - if len(msgLine) == 0 { - return "" - } - - // we can safely assume that it will not return any error, since there shouldn't be any special HTML. - renderedMessage, err := markup.PostProcessCommitMessageSubject(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), urlDefault, template.HTMLEscapeString(msgLine)) - if err != nil { - log.Error("PostProcessCommitMessageSubject: %v", err) - return "" - } - return renderCodeBlock(template.HTML(renderedMessage)) + msgLine := strings.TrimSpace(msg) + msgLine, _, _ = strings.Cut(msgLine, "\n") + msgLine = strings.TrimSpace(msgLine) + rctx := renderhelper.NewRenderContextRepoComment(ut.ctx, repo) + rendered := markup.PostProcessCommitMessageSubject(rctx, urlDefault, htmlutil.EscapeString(msgLine)) + return renderCodeBlock(rendered) } // RenderCommitBody extracts the body of a commit message without its title. func (ut *RenderUtils) RenderCommitBody(msg string, repo *repo.Repository) template.HTML { _, body, _ := strings.Cut(strings.TrimSpace(msg), "\n") - body = strings.TrimFunc(body, unicode.IsSpace) + body = strings.TrimSpace(body) if body == "" { return "" } - rctx := renderhelper.NewRenderContextRepoComment(ut.ctx, repo) - htmlContent := template.HTML(template.HTMLEscapeString(body)) - renderedMessage, err := markup.PostProcessCommitMessage(rctx, htmlContent) - if err != nil { - log.Error("PostProcessCommitMessage: %v", err) - return "" - } + renderedMessage := markup.PostProcessCommitMessage(rctx, htmlutil.EscapeString(body)) return renderedMessage } @@ -108,25 +86,15 @@ func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML { // RenderIssueTitle renders issue/pull title with defined post processors func (ut *RenderUtils) RenderIssueTitle(text string, repo *repo.Repository) template.HTML { // wrap "`…`" in before post-processing so code-span content stays literal, like comment bodies - htmlWithCode := renderCodeBlock(template.HTML(template.HTMLEscapeString(text))) - renderedText, err := markup.PostProcessIssueTitle(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), string(htmlWithCode)) - if err != nil { - log.Error("PostProcessIssueTitle: %v", err) - return "" - } - return template.HTML(renderedText) + htmlWithCode := renderCodeBlock(htmlutil.EscapeString(text)) + return markup.PostProcessIssueTitle(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), htmlWithCode) } // RenderIssueSimpleTitle only renders with emoji and inline code block func (ut *RenderUtils) RenderIssueSimpleTitle(text string) template.HTML { // see RenderIssueTitle: wrap code spans before processing emoji - htmlWithCode := renderCodeBlock(template.HTML(template.HTMLEscapeString(text))) - renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), string(htmlWithCode)) - if err != nil { - log.Error("RenderIssueSimpleTitle: %v", err) - return "" - } - return template.HTML(renderedText) + htmlWithCode := renderCodeBlock(htmlutil.EscapeString(text)) + return markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), htmlWithCode) } func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { @@ -202,12 +170,7 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { // RenderEmoji renders html text with emoji post processors func (ut *RenderUtils) RenderEmoji(text string) template.HTML { - renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), template.HTMLEscapeString(text)) - if err != nil { - log.Error("RenderEmoji: %v", err) - return "" - } - return template.HTML(renderedText) + return markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), htmlutil.EscapeString(text)) } // reactionToEmoji renders emoji for use in reactions @@ -332,3 +295,134 @@ func (ut *RenderUtils) RenderUnicodeEscapeToggleTd(combined, escapeStatus *chars } return `` + ut.RenderUnicodeEscapeToggleButton(escapeStatus) + `` } + +func renderAvatarStackViewEmailLink(data *user_model.AvatarStackData, email string) template.URL { + if data.SearchByEmailLink != "" && email != "" { + return template.URL(strings.ReplaceAll(data.SearchByEmailLink, "{email}", url.QueryEscape(email))) + } + return "" +} + +func (ut *RenderUtils) participantHref(data *user_model.AvatarStackData, participant *user_model.CommitParticipant) template.URL { + if href := renderAvatarStackViewEmailLink(data, participant.GitIdentity.Email); href != "" { + return href + } + if participant.GiteaUser != nil { + return template.URL(participant.GiteaUser.HomeLink()) + } else if participant.GitIdentity.Email != "" { + return template.URL("mailto:" + participant.GitIdentity.Email) + } + return "" +} + +func (ut *RenderUtils) participantAvatar(participant *user_model.CommitParticipant) template.HTML { + if participant.GiteaUser != nil { + return ut.avatarUtils.Avatar(participant.GiteaUser, 20) + } + return ut.avatarUtils.AvatarByEmail(participant.GitIdentity.Email, participant.GitIdentity.Name, 20) +} + +func participantName(participant *user_model.CommitParticipant) string { + if participant.GiteaUser != nil { + return participant.GiteaUser.GetDisplayName() + } + return participant.GitIdentity.Name +} + +const renderAvatarStackMaxVisible = 10 + +// AvatarStack renders overlapping avatars for the stack participants. It emits children in reverse +// so CSS `flex-direction: row-reverse` places the primary (Participants[0]) leftmost and last-painted (on top). +func (ut *RenderUtils) AvatarStack(data *user_model.AvatarStackData) template.HTML { + visible := data.Participants + overflow := len(visible) - renderAvatarStackMaxVisible + if overflow > 0 { + visible = visible[:renderAvatarStackMaxVisible] + } + + var b htmlutil.HTMLBuilder + b.WriteHTML(``) + if overflow > 0 { + b.WriteFormat(`+%d`, overflow, overflow) + } + + // FIXME: such "backward" breaks a11y like screen readers + for _, participant := range slices.Backward(visible) { + ut.writeAvatarStackItem(&b, data, participant) + } + b.WriteHTML(``) + return b.HTMLString() +} + +func (ut *RenderUtils) writeAvatarStackItem(b *htmlutil.HTMLBuilder, data *user_model.AvatarStackData, participant *user_model.CommitParticipant) { + avatar := ut.participantAvatar(participant) + if href := ut.participantHref(data, participant); href != "" { + b.WriteFormat(`%s`, href, avatar) + } else { + b.WriteFormat(`%s`, avatar) + } +} + +func (ut *RenderUtils) AvatarStackPushCommit(pushCommit *repository.PushCommit) template.HTML { + fakeGitCommit := git.Commit{ + CommitMessage: git.CommitMessage{MessageRaw: pushCommit.Message}, + Author: &git.Signature{Name: pushCommit.AuthorName, Email: pushCommit.AuthorEmail}, + // there is no way to know the real committer, but the field can't be nil + Committer: &git.Signature{Name: pushCommit.AuthorName, Email: pushCommit.AuthorEmail}, + } + data := user_model.BuildAvatarStackData(ut.ctx, fakeGitCommit.AllParticipantIdentities(), nil) + return ut.AvatarStack(data) +} + +// AvatarStackWithNames renders the avatar stack plus a label: `name` / `a and b` / `N people` (opens popup). +func (ut *RenderUtils) AvatarStackWithNames(data *user_model.AvatarStackData) template.HTML { + locale := ut.ctx.Value(translation.ContextKey).(translation.Locale) + participants := data.Participants + + var b htmlutil.HTMLBuilder + b.WriteHTML(``) + b.WriteHTML(ut.AvatarStack(data)) + + switch len(participants) { + case 1: + b.WriteHTML(ut.participantNameLink(data, participants[0])) + case 2: + b.WriteHTML(ut.participantNameLink(data, participants[0])) + b.WriteFormat(`%s`, locale.Tr("repo.commits.avatar_stack_and")) + b.WriteHTML(ut.participantNameLink(data, participants[1])) + default: + b.WriteFormat(``, + locale.Tr("repo.commits.avatar_stack_people", len(participants))) + b.WriteHTML(`
`) + for _, participant := range participants { + b.WriteHTML(ut.participantPopupRow(data, participant)) + } + b.WriteHTML(`
`) + } + + b.WriteHTML(`
`) + return b.HTMLString() +} + +// participantNameLink prefers (in order): commits-by-author search, `GetShortDisplayNameLinkHTML` (keeps alt-name tooltip), `mailto:`, bare name. +func (ut *RenderUtils) participantNameLink(data *user_model.AvatarStackData, participant *user_model.CommitParticipant) template.HTML { + if href := renderAvatarStackViewEmailLink(data, participant.GitIdentity.Email); href != "" { + return htmlutil.HTMLFormat(`%s`, href, participantName(participant)) + } + if participant.GiteaUser != nil { + return participant.GiteaUser.GetShortDisplayNameLinkHTML() + } + if participant.GitIdentity.Email != "" { + return htmlutil.HTMLFormat(`%s`, participant.GitIdentity.Email, participant.GitIdentity.Name) + } + return template.HTML(template.HTMLEscapeString(participant.GitIdentity.Name)) +} + +func (ut *RenderUtils) participantPopupRow(data *user_model.AvatarStackData, participant *user_model.CommitParticipant) template.HTML { + avatar := ut.participantAvatar(participant) + name := participantName(participant) + if href := ut.participantHref(data, participant); href != "" { + return htmlutil.HTMLFormat(`%s%s`, href, avatar, name) + } + return htmlutil.HTMLFormat(`%s%s`, avatar, name) +} diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go index 50a443c7468..1db87feb798 100644 --- a/modules/templates/util_render_test.go +++ b/modules/templates/util_render_test.go @@ -7,15 +7,19 @@ import ( "context" "html/template" "os" + "strconv" "strings" "testing" + "gitea.dev/models/gituser" "gitea.dev/models/issues" "gitea.dev/models/repo" user_model "gitea.dev/models/user" + "gitea.dev/modules/git" "gitea.dev/modules/markup" "gitea.dev/modules/reqctx" "gitea.dev/modules/setting" + "gitea.dev/modules/setting/config" "gitea.dev/modules/test" "gitea.dev/modules/translation" @@ -131,24 +135,24 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit }) t.Run("RenderCommitMessage", func(t *testing.T) { - expected := `space @mention-user ` + expected := `space @mention-user` assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessage(testInput(), mockRepo)) }) t.Run("RenderCommitMessageLinkSubject", func(t *testing.T) { - expected := `space @mention-user` + expected := `space @mention-user` assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", mockRepo)) }) t.Run("RenderCommitMessageLinkSubjectURLOnly", func(t *testing.T) { // a bare URL in the subject must not hijack the default link - expected := `https://example.com/file.bin` + expected := `https://example.com/file.bin` assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject("https://example.com/file.bin", "https://example.com/link", mockRepo)) }) t.Run("RenderCommitMessageLinkSubjectPartialURL", func(t *testing.T) { // a URL embedded in larger subject text still becomes its own link - expected := `see https://example.com/x here` + expected := `see https://example.com/x here` assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject("see https://example.com/x here", "https://example.com/link", mockRepo)) }) @@ -298,3 +302,52 @@ func TestUserMention(t *testing.T) { rendered := newTestRenderUtils(t).MarkdownToHtml("@no-such-user @mention-user @mention-user") assert.Equal(t, `

@no-such-user @mention-user @mention-user

`, strings.TrimSpace(string(rendered))) } + +func TestAvatarStack(t *testing.T) { + defer test.MockVariableValue(&config.SkipDatabaseConfig, true)() + + ut := newTestRenderUtils(t) + mkCo := func(name, email string) *git.CommitIdentity { + return &git.CommitIdentity{Name: name, Email: email} + } + authorSig := mkCo("Alice", "alice@example.com") + mkData := func(co ...*git.CommitIdentity) *gituser.AvatarStackData { + all := append([]*git.CommitIdentity{authorSig}, co...) + return gituser.BuildAvatarStackData(t.Context(), all, &user_model.EmailUserMap{}) + } + + t.Run("lone author renders bare name, no label", func(t *testing.T) { + got := string(ut.AvatarStackWithNames(mkData())) + assert.Contains(t, got, ``) + assert.Contains(t, got, "Alice") + assert.NotContains(t, got, "avatar_stack_and") + assert.NotContains(t, got, "avatar_stack_people") + }) + + t.Run("two participants use and label", func(t *testing.T) { + got := string(ut.AvatarStackWithNames(mkData(mkCo("Bob", "bob@example.com")))) + assert.Contains(t, got, "repo.commits.avatar_stack_and") + assert.Contains(t, got, "Bob") + assert.NotContains(t, got, "avatar_stack_people") + assert.Contains(t, got, ``) + }) + + t.Run("three participants switch to N people label with tippy popup", func(t *testing.T) { + got := string(ut.AvatarStackWithNames(mkData(mkCo("Bob", "bob@example.com"), mkCo("Carol", "carol@example.com")))) + assert.Contains(t, got, "repo.commits.avatar_stack_people:3") + assert.NotContains(t, got, "repo.commits.avatar_stack_and") + assert.Contains(t, got, `data-global-init="initAvatarStackPopup"`) + assert.Contains(t, got, `
`) + assert.Contains(t, got, `class="avatar-stack-popup"`) + }) + + t.Run("overflow chip renders beyond 10 participants", func(t *testing.T) { + cos := make([]*git.CommitIdentity, 0, renderAvatarStackMaxVisible+1) + for i := range renderAvatarStackMaxVisible + 1 { + cos = append(cos, mkCo("X", strconv.Itoa(i)+"@example.com")) + } + got := ut.AvatarStack(gituser.BuildAvatarStackData(t.Context(), cos, &user_model.EmailUserMap{})) + assert.Contains(t, got, `class="avatar-stack-overflow-chip`) + assert.Contains(t, got, "+1") + }) +} diff --git a/modules/zstd/zstd.go b/modules/zstd/zstd.go index d2249447d62..00cf2ea82e0 100644 --- a/modules/zstd/zstd.go +++ b/modules/zstd/zstd.go @@ -60,7 +60,7 @@ func (r *Reader) Close() error { type SeekableWriter struct { buf []byte n int - w seekable.Writer + w *seekable.Writer } var _ io.WriteCloser = (*SeekableWriter)(nil) @@ -114,7 +114,7 @@ func (w *SeekableWriter) Close() error { } type SeekableReader struct { - r seekable.Reader + r *seekable.Reader c func() error } diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index fdef5813580..90c7c71fb7d 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -1321,6 +1321,7 @@ "repo.editor.fork_branch_exists": "Branch \"%s\" already exists in your fork. Please choose a new branch name.", "repo.commits.desc": "Browse source code change history.", "repo.commits.commits": "Commits", + "repo.commits.history_enable_follow_renames": "Include renames", "repo.commits.no_commits": "No commits in common. \"%s\" and \"%s\" have entirely different histories.", "repo.commits.nothing_to_compare": "There are no differences to show.", "repo.commits.search.tooltip": "You can prefix keywords with \"author:\", \"committer:\", \"after:\", or \"before:\", e.g. \"revert author:Alice before:2019-01-13\".", @@ -2204,10 +2205,10 @@ "repo.settings.trust_model.collaborator.desc": "Valid signatures by collaborators of this repository will be marked \"trusted\", whether they match the committer or not. Otherwise, valid signatures will be marked \"untrusted\" if the signature matches the committer and \"unmatched\" if not.", "repo.settings.trust_model.committer": "Committer", "repo.settings.trust_model.committer.long": "Committer: Trust signatures that match committers. This matches GitHub's behavior and will force commits signed by Gitea to have Gitea as the committer.", - "repo.settings.trust_model.committer.desc": "Valid signatures will only be marked \"trusted\" if they match the committer, otherwise they will be marked \"unmatched\". This forces Gitea to be the committer on signed commits, with the actual committer marked as Co-authored-by: and Co-committed-by: trailer in the commit. The default Gitea key must match a user in the database.", + "repo.settings.trust_model.committer.desc": "Valid signatures will only be marked \"trusted\" if they match the committer, otherwise they will be marked \"unmatched\". This forces Gitea to be the committer on signed commits, with the actual committer marked as a Co-authored-by: trailer in the commit. The default Gitea key must match a user in the database.", "repo.settings.trust_model.collaboratorcommitter": "Collaborator+Committer", "repo.settings.trust_model.collaboratorcommitter.long": "Collaborator+Committer: Trust signatures by collaborators which match the committer", - "repo.settings.trust_model.collaboratorcommitter.desc": "Valid signatures by collaborators of this repository will be marked \"trusted\" if they match the committer. Otherwise, valid signatures will be marked \"untrusted\" if the signature matches the committer and \"unmatched\" otherwise. This will force Gitea to be marked as the committer on signed commits, with the actual committer marked as Co-Authored-By: and Co-Committed-By: trailer in the commit. The default Gitea key must match a user in the database.", + "repo.settings.trust_model.collaboratorcommitter.desc": "Valid signatures by collaborators of this repository will be marked \"trusted\" if they match the committer. Otherwise, valid signatures will be marked \"untrusted\" if the signature matches the committer and \"unmatched\" otherwise. This will force Gitea to be marked as the committer on signed commits, with the actual committer marked as a Co-Authored-By: trailer in the commit. The default Gitea key must match a user in the database.", "repo.settings.wiki_delete": "Delete Wiki Data", "repo.settings.wiki_delete_desc": "Deleting repository wiki data is permanent and cannot be undone.", "repo.settings.wiki_delete_notices_1": "- This will permanently delete and disable the repository wiki for %s.", @@ -2598,6 +2599,9 @@ "repo.diff.review.reject": "Request changes", "repo.diff.review.self_approve": "Pull request authors can't approve their own pull request", "repo.diff.committed_by": "committed by", + "repo.diff.coauthored_by": "co-authored by", + "repo.commits.avatar_stack_and": "and", + "repo.commits.avatar_stack_people": "%d people", "repo.diff.protected": "Protected", "repo.diff.image.side_by_side": "Side by Side", "repo.diff.image.swipe": "Swipe", @@ -3773,6 +3777,7 @@ "actions.runs.no_matching_online_runner_helper": "No matching online runner with label: %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", "actions.runs.actor": "Actor", "actions.runs.status": "Status", "actions.runs.actors_no_select": "All actors", @@ -3793,13 +3798,17 @@ "actions.runs.view_workflow_file": "View workflow file", "actions.runs.summary": "Summary", "actions.runs.all_jobs": "All jobs", + "actions.runs.job_summaries": "Job summaries", "actions.runs.expand_caller_jobs": "Show jobs of this reusable workflow caller", "actions.runs.collapse_caller_jobs": "Hide jobs of this reusable workflow caller", "actions.runs.attempt": "Attempt", "actions.runs.latest": "Latest", "actions.runs.latest_attempt": "Latest attempt", "actions.runs.triggered_via": "Triggered via %s", - "actions.runs.total_duration": "Total duration:", + "actions.runs.rerun_triggered": "Re-run triggered", + "actions.runs.back_to_pull_request": "Back to pull request", + "actions.runs.back_to_workflow": "Back to workflow", + "actions.runs.total_duration": "Total duration", "actions.runs.workflow_dependencies": "Workflow Dependencies", "actions.runs.graph_jobs_count_1": "%d job", "actions.runs.graph_jobs_count_n": "%d jobs", diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index 3e3fd8746f3..9bff2c24be8 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -1321,6 +1321,7 @@ "repo.editor.fork_branch_exists": "Tá brainse \"%s\" ann cheana féin i do fhorc. Roghnaigh ainm brainse nua le do thoil.", "repo.commits.desc": "Brabhsáil stair athraithe cód foinse.", "repo.commits.commits": "Tiomáintí", + "repo.commits.history_enable_follow_renames": "Cuir athainmneacha san áireamh", "repo.commits.no_commits": "Níl aon ghealltanas i gcoiteann. Tá stair iomlán difriúil ag \"%s\" agus \"%s\".", "repo.commits.nothing_to_compare": "Níl aon difríochtaí le taispeáint.", "repo.commits.search.tooltip": "Is féidir eochairfhocail a réamhfhostú le “údar:”, “committer:”, “after:”, nó “before:”, e.g. \"fill an t-údar:Alice roimh: 2019-01-13\".", diff --git a/package.json b/package.json index c56dd03b083..cbd944222bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "type": "module", - "packageManager": "pnpm@11.2.1", + "packageManager": "pnpm@11.5.1", "engines": { "node": ">= 22.18.0", "pnpm": ">= 11.0.0" @@ -10,7 +10,7 @@ "@citation-js/plugin-bibtex": "0.7.21", "@citation-js/plugin-csl": "0.7.22", "@citation-js/plugin-software-formats": "0.6.2", - "@codemirror/autocomplete": "6.20.2", + "@codemirror/autocomplete": "6.20.3", "@codemirror/commands": "6.10.3", "@codemirror/lang-json": "6.0.2", "@codemirror/lang-markdown": "6.5.0", @@ -28,7 +28,7 @@ "@lezer/highlight": "1.2.3", "@mcaptcha/vanilla-glue": "0.1.0-rc2", "@mermaid-js/layout-elk": "0.2.1", - "@primer/octicons": "19.26.0", + "@primer/octicons": "19.28.0", "@replit/codemirror-indentation-markers": "6.5.3", "@replit/codemirror-lang-nix": "6.0.1", "@replit/codemirror-lang-svelte": "6.0.0", @@ -45,19 +45,19 @@ "colord": "2.9.3", "compare-versions": "6.1.1", "cropperjs": "1.6.2", - "dayjs": "1.11.20", + "dayjs": "1.11.21", "easymde": "2.21.0", "esbuild": "0.28.0", "idiomorph": "0.7.4", "jquery": "4.0.0", - "js-yaml": "4.1.1", - "katex": "0.16.47", + "js-yaml": "4.2.0", + "katex": "0.17.0", "mermaid": "11.15.0", "online-3d-viewer": "0.18.0", "pdfobject": "2.3.1", "perfect-debounce": "2.1.0", "postcss": "8.5.15", - "rolldown-license-plugin": "3.0.7", + "rolldown-license-plugin": "3.0.9", "sortablejs": "1.15.7", "swagger-ui-dist": "5.32.6", "tailwindcss": "3.4.19", @@ -67,15 +67,15 @@ "tributejs": "5.1.3", "uint8-to-base64": "0.2.1", "vanilla-colorful": "0.7.2", - "vite": "8.0.13", + "vite": "8.0.16", "vite-string-plugin": "2.0.4", - "vue": "3.5.34", + "vue": "3.5.35", "vue-bar-graph": "2.2.0", "vue-chartjs": "5.3.3" }, "devDependencies": { - "@eslint-community/eslint-plugin-eslint-comments": "4.7.1", - "@eslint/json": "1.2.0", + "@eslint-community/eslint-plugin-eslint-comments": "4.7.2", + "@eslint/json": "2.0.0", "@playwright/test": "1.60.0", "@stylistic/eslint-plugin": "5.10.0", "@stylistic/stylelint-plugin": "5.2.0", @@ -89,11 +89,11 @@ "@types/swagger-ui-dist": "3.30.6", "@types/throttle-debounce": "5.0.2", "@types/toastify-js": "1.12.4", - "@typescript-eslint/parser": "8.59.4", + "@typescript-eslint/parser": "8.60.1", "@vitejs/plugin-vue": "6.0.7", - "@vitest/eslint-plugin": "1.6.17", - "eslint": "10.4.0", - "eslint-import-resolver-typescript": "4.4.4", + "@vitest/eslint-plugin": "1.6.19", + "eslint": "10.4.1", + "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", @@ -102,15 +102,14 @@ "eslint-plugin-regexp": "3.1.0", "eslint-plugin-sonarjs": "4.0.3", "eslint-plugin-unicorn": "64.0.0", - "eslint-plugin-vue": "10.9.1", - "eslint-plugin-vue-scoped-css": "3.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.9.0", "jiti": "2.7.0", "markdownlint-cli": "0.48.0", - "material-icon-theme": "5.34.0", - "nolyfill": "1.0.44", + "material-icon-theme": "5.35.0", "postcss-html": "1.8.1", "spectral-cli-bundle": "1.0.8", "stylelint": "17.12.0", @@ -120,9 +119,9 @@ "stylelint-value-no-unknown-custom-properties": "6.1.1", "svgo": "4.0.1", "typescript": "6.0.3", - "typescript-eslint": "8.59.4", - "updates": "17.16.13", - "vitest": "4.1.7", - "vue-tsc": "3.3.1" + "typescript-eslint": "8.60.1", + "updates": "17.17.3", + "vitest": "4.1.8", + "vue-tsc": "3.3.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e96e6a6174b..948bb2ba5c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,28 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - array-includes: npm:@nolyfill/array-includes@^1 - array.prototype.findlastindex: npm:@nolyfill/array.prototype.findlastindex@^1 - array.prototype.flat: npm:@nolyfill/array.prototype.flat@^1 - array.prototype.flatmap: npm:@nolyfill/array.prototype.flatmap@^1 - es-aggregate-error: npm:@nolyfill/es-aggregate-error@^1 - hasown: npm:@nolyfill/hasown@^1 - is-core-module: npm:@nolyfill/is-core-module@^1 - object.assign: npm:@nolyfill/object.assign@^1 - object.fromentries: npm:@nolyfill/object.fromentries@^1 - object.groupby: npm:@nolyfill/object.groupby@^1 - object.values: npm:@nolyfill/object.values@^1 - safe-buffer: npm:@nolyfill/safe-buffer@^1 - safe-regex-test: npm:@nolyfill/safe-regex-test@^1 - safer-buffer: npm:@nolyfill/safer-buffer@^1 - string.prototype.includes: npm:@nolyfill/string.prototype.includes@^1 - string.prototype.trimend: npm:@nolyfill/string.prototype.trimend@^1 - object-keys: npm:@nolyfill/object-keys@^1 - object.entries: npm:@nolyfill/object.entries@^1 - abab: npm:@nolyfill/abab@^1 - es-set-tostringtag: npm:@nolyfill/es-set-tostringtag@^1 - importers: .: @@ -43,8 +21,8 @@ importers: specifier: 0.6.2 version: 0.6.2 '@codemirror/autocomplete': - specifier: 6.20.2 - version: 6.20.2 + specifier: 6.20.3 + version: 6.20.3 '@codemirror/commands': specifier: 6.10.3 version: 6.10.3 @@ -97,26 +75,26 @@ importers: specifier: 0.2.1 version: 0.2.1(mermaid@11.15.0) '@primer/octicons': - specifier: 19.26.0 - version: 19.26.0 + specifier: 19.28.0 + version: 19.28.0 '@replit/codemirror-indentation-markers': specifier: 6.5.3 version: 6.5.3(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0) '@replit/codemirror-lang-nix': specifier: 6.0.1 - version: 6.0.1(@codemirror/autocomplete@6.20.2)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@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.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.10) '@replit/codemirror-lang-svelte': specifier: 6.0.0 - version: 6.0.0(@codemirror/autocomplete@6.20.2)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@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.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.10) '@replit/codemirror-vscode-keymap': specifier: 6.0.2 - version: 6.0.2(@codemirror/autocomplete@6.20.2)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0) + version: 6.0.2(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0) '@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.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.34(typescript@6.0.3)) + version: 6.0.7(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3)) ansi_up: specifier: 6.0.6 version: 6.0.6 @@ -128,7 +106,7 @@ importers: version: 4.5.1 chartjs-adapter-dayjs-4: specifier: 1.0.4 - version: 1.0.4(chart.js@4.5.1)(dayjs@1.11.20) + version: 1.0.4(chart.js@4.5.1)(dayjs@1.11.21) chartjs-plugin-zoom: specifier: 2.2.0 version: 2.2.0(chart.js@4.5.1) @@ -148,8 +126,8 @@ importers: specifier: 1.6.2 version: 1.6.2 dayjs: - specifier: 1.11.20 - version: 1.11.20 + specifier: 1.11.21 + version: 1.11.21 easymde: specifier: 2.21.0 version: 2.21.0 @@ -163,11 +141,11 @@ importers: specifier: 4.0.0 version: 4.0.0 js-yaml: - specifier: 4.1.1 - version: 4.1.1 + specifier: 4.2.0 + version: 4.2.0 katex: - specifier: 0.16.47 - version: 0.16.47 + specifier: 0.17.0 + version: 0.17.0 mermaid: specifier: 11.15.0 version: 11.15.0 @@ -184,8 +162,8 @@ importers: specifier: 8.5.15 version: 8.5.15 rolldown-license-plugin: - specifier: 3.0.7 - version: 3.0.7(rolldown@1.0.1) + specifier: 3.0.9 + version: 3.0.9(rolldown@1.0.3) sortablejs: specifier: 1.15.7 version: 1.15.7 @@ -214,33 +192,33 @@ importers: specifier: 0.7.2 version: 0.7.2 vite: - specifier: 8.0.13 - version: 8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + specifier: 8.0.16 + version: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) vite-string-plugin: specifier: 2.0.4 - version: 2.0.4(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + version: 2.0.4(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) vue: - specifier: 3.5.34 - version: 3.5.34(typescript@6.0.3) + specifier: 3.5.35 + version: 3.5.35(typescript@6.0.3) vue-bar-graph: specifier: 2.2.0 version: 2.2.0(typescript@6.0.3) vue-chartjs: specifier: 5.3.3 - version: 5.3.3(chart.js@4.5.1)(vue@3.5.34(typescript@6.0.3)) + version: 5.3.3(chart.js@4.5.1)(vue@3.5.35(typescript@6.0.3)) devDependencies: '@eslint-community/eslint-plugin-eslint-comments': - specifier: 4.7.1 - version: 4.7.1(eslint@10.4.0(jiti@2.7.0)) + specifier: 4.7.2 + version: 4.7.2(eslint@10.4.1(jiti@2.7.0)) '@eslint/json': - specifier: 1.2.0 - version: 1.2.0 + specifier: 2.0.0 + version: 2.0.0 '@playwright/test': specifier: 1.60.0 version: 1.60.0 '@stylistic/eslint-plugin': specifier: 5.10.0 - version: 5.10.0(eslint@10.4.0(jiti@2.7.0)) + version: 5.10.0(eslint@10.4.1(jiti@2.7.0)) '@stylistic/stylelint-plugin': specifier: 5.2.0 version: 5.2.0(stylelint@17.12.0(typescript@6.0.3)) @@ -275,50 +253,50 @@ importers: specifier: 1.12.4 version: 1.12.4 '@typescript-eslint/parser': - specifier: 8.59.4 - version: 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + specifier: 8.60.1 + version: 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) '@vitest/eslint-plugin': - specifier: 1.6.17 - version: 1.6.17(@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))) + specifier: 1.6.19 + version: 1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(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.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))) eslint: - specifier: 10.4.0 - version: 10.4.0(jiti@2.7.0) + specifier: 10.4.1 + version: 10.4.1(jiti@2.7.0) eslint-import-resolver-typescript: - specifier: 4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.0(jiti@2.7.0)) + specifier: 4.4.5 + version: 4.4.5(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(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.0(jiti@2.7.0)) + 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.0(jiti@2.7.0)) + 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.4)(eslint@10.4.0(jiti@2.7.0)) + version: 6.0.0(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)) eslint-plugin-import-x: specifier: 4.16.2 - version: 4.16.2(@typescript-eslint/utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)) + version: 4.16.2(@typescript-eslint/utils@8.60.1(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-playwright: specifier: 2.10.4 - version: 2.10.4(eslint@10.4.0(jiti@2.7.0)) + version: 2.10.4(eslint@10.4.1(jiti@2.7.0)) eslint-plugin-regexp: specifier: 3.1.0 - version: 3.1.0(eslint@10.4.0(jiti@2.7.0)) + version: 3.1.0(eslint@10.4.1(jiti@2.7.0)) eslint-plugin-sonarjs: specifier: 4.0.3 - version: 4.0.3(eslint@10.4.0(jiti@2.7.0)) + version: 4.0.3(eslint@10.4.1(jiti@2.7.0)) eslint-plugin-unicorn: specifier: 64.0.0 - version: 64.0.0(eslint@10.4.0(jiti@2.7.0)) + version: 64.0.0(eslint@10.4.1(jiti@2.7.0)) eslint-plugin-vue: - specifier: 10.9.1 - version: 10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.4.0(jiti@2.7.0)))(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0))) + 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.60.1(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-scoped-css: - specifier: 3.1.0 - version: 3.1.0(eslint@10.4.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0))) + 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))) eslint-plugin-wc: specifier: 3.1.0 - version: 3.1.0(eslint@10.4.0(jiti@2.7.0)) + version: 3.1.0(eslint@10.4.1(jiti@2.7.0)) globals: specifier: 17.6.0 version: 17.6.0 @@ -332,11 +310,8 @@ importers: specifier: 0.48.0 version: 0.48.0 material-icon-theme: - specifier: 5.34.0 - version: 5.34.0 - nolyfill: - specifier: 1.0.44 - version: 1.0.44 + specifier: 5.35.0 + version: 5.35.0 postcss-html: specifier: 1.8.1 version: 1.8.1 @@ -365,17 +340,17 @@ importers: specifier: 6.0.3 version: 6.0.3 typescript-eslint: - specifier: 8.59.4 - version: 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + specifier: 8.60.1 + version: 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) updates: - specifier: 17.16.13 - version: 17.16.13 + specifier: 17.17.3 + version: 17.17.3 vitest: - specifier: 4.1.7 - version: 4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + specifier: 4.1.8 + version: 4.1.8(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) vue-tsc: - specifier: 3.3.1 - version: 3.3.1(typescript@6.0.3) + specifier: 3.3.3 + version: 3.3.3(typescript@6.0.3) packages: @@ -386,36 +361,36 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.3': - resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/runtime@7.29.2': - resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} - '@cacheable/memory@2.0.8': - resolution: {integrity: sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==} + '@cacheable/memory@2.0.9': + resolution: {integrity: sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==} '@cacheable/utils@2.4.1': resolution: {integrity: sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==} @@ -471,8 +446,8 @@ packages: resolution: {integrity: sha512-3XQOO3u4WXY/7AWZyQ+9SuBzS8bYTlJ+NF1uCgrZO64g36nK5iIc5YV9cBl2TL2QhHF6S36nvAsXsj5fX9FeHw==} engines: {node: '>=14.0.0'} - '@codemirror/autocomplete@6.20.2': - resolution: {integrity: sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==} + '@codemirror/autocomplete@6.20.3': + resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} '@codemirror/commands@6.10.3': resolution: {integrity: sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==} @@ -561,8 +536,8 @@ packages: '@codemirror/view@6.43.0': resolution: {integrity: sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==} - '@csstools/css-calc@3.2.0': - resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + '@csstools/css-calc@3.2.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} engines: {node: '>=20.19.0'} peerDependencies: '@csstools/css-parser-algorithms': ^4.0.0 @@ -574,8 +549,8 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.3': - resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + '@csstools/css-syntax-patches-for-csstree@1.1.4': + resolution: {integrity: sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==} peerDependencies: css-tree: ^3.2.1 peerDependenciesMeta: @@ -773,8 +748,8 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-plugin-eslint-comments@4.7.1': - resolution: {integrity: sha512-Ql2nJFwA8wUGpILYGOQaT1glPsmvEwE0d+a+l7AALLzQvInqdbXJdx7aSu0DpUX9dB1wMVBMhm99/++S3MdEtQ==} + '@eslint-community/eslint-plugin-eslint-comments@4.7.2': + resolution: {integrity: sha512-LF03qURSwEWm2dz5wtdDCzNk+7Opl0X7q6I3undsaIuNsEiNvRV3BCtqu14Q/6Pzg1tBj44LcxpW2EpSLZStZw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 @@ -822,20 +797,16 @@ packages: resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/json@1.2.0': - resolution: {integrity: sha512-CEFEyNgvzu8zn5QwVYDg3FaG+ZKUeUsNYitFpMYJAqoAlnw68EQgNbUfheSmexZr4n0wZPrAkPLuvsLaXO6wRw==} + '@eslint/json@2.0.0': + resolution: {integrity: sha512-P32ZJMIopNWQd1SFhd0tgjfA/hgzUuVSqHmMi2273QaLWHWimXq6V+qL4DNKnjGzO/aNECtYW+rEJ/pWB6uP+w==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/object-schema@3.0.5': resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.6.1': - resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/plugin-kit@0.7.1': - resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@github/browserslist-config@1.0.0': @@ -880,8 +851,8 @@ packages: '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} - '@iconify/utils@3.1.1': - resolution: {integrity: sha512-MwzoDtw9rO1x+qfgLTV/IVXsHDBqeYZoMIQC8SfxfYSlaSUG+oWiAcoiB1yajAda6mqblm4/1/w2E8tRu7a7Tw==} + '@iconify/utils@3.1.3': + resolution: {integrity: sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==} '@jest/environment@29.7.0': resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} @@ -927,8 +898,8 @@ packages: '@lezer/common@1.5.2': resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} - '@lezer/cpp@1.1.5': - resolution: {integrity: sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==} + '@lezer/cpp@1.1.6': + resolution: {integrity: sha512-vh9gWWJOXFVY8HBHK3Twzq8MgwG2iN4GSyzBP9sCGTe37P15x2R14VaBQk0VA0ezTRN1KHYBBsHhvpGZ2Xy/pA==} '@lezer/css@1.3.3': resolution: {integrity: sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==} @@ -954,14 +925,14 @@ packages: '@lezer/lr@1.4.10': resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} - '@lezer/markdown@1.6.3': - resolution: {integrity: sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==} + '@lezer/markdown@1.6.4': + resolution: {integrity: sha512-N0SxazMj4k65DBfaf1azqtMZd6u7MqluP84/NZnB/io8Td9aleFmAhz9hcbvSfsxT5tdYlJ5qgv5aMJGY4zEtA==} '@lezer/php@1.0.5': resolution: {integrity: sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==} - '@lezer/python@1.1.18': - resolution: {integrity: sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==} + '@lezer/python@1.1.19': + resolution: {integrity: sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ==} '@lezer/rust@1.0.2': resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==} @@ -992,9 +963,6 @@ packages: '@mermaid-js/parser@1.1.1': resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} - '@napi-rs/wasm-runtime@0.2.12': - resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: @@ -1013,90 +981,15 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nolyfill/abab@1.0.44': - resolution: {integrity: sha512-ZQFi9RSKHKnXM4lLtazYx7otLcqEpQ6sSzs7yzb6zlcimyFIv3CV4s2G+CaA3GnPDTmUFugCn2wWap4jynGQow==} - engines: {node: '>=12.4.0'} - - '@nolyfill/array-includes@1.0.44': - resolution: {integrity: sha512-IVEqpEgFbLaU0hUoMwJYXNSdi6lq+FxHdxd8xTKDLxh8k6u5YNGz4Bo6bT46l7p0x8PbJmHViBtngqhvE528fA==} - engines: {node: '>=12.4.0'} - - '@nolyfill/array.prototype.findlastindex@1.0.44': - resolution: {integrity: sha512-BLeHS3SulsR3iFxxETL9q21lArV2KS7lh2wcUnhue1ppx19xah1W7MdFxepyeGbM3Umk9S90snfboXAds5HkTg==} - engines: {node: '>=12.4.0'} - - '@nolyfill/array.prototype.flat@1.0.44': - resolution: {integrity: sha512-HnOqOT4te0l+XU9UKhy3ry+pc+ZRNsUJFR7omMEtjXf4+dq6oXmIBk7vR35+hSTk4ldjwm/27jwV3ZIGp3l4IQ==} - engines: {node: '>=12.4.0'} - - '@nolyfill/array.prototype.flatmap@1.0.44': - resolution: {integrity: sha512-P6OsaEUrpBJ9NdNekFDQVM9LOFHPDKSJzwOWRBaC6LqREX+4lkZT2Q+to78R6aG6atuOQsxBVqPjMGCKjWdvyQ==} - engines: {node: '>=12.4.0'} - - '@nolyfill/es-set-tostringtag@1.0.44': - resolution: {integrity: sha512-Qfiv/3wI+mKSPCgU8Fg/7Auu4os00St4GwyLqCCZ/oBD4W00itWUkl9Rq1MCGS+VXYDnTobvrc6AvPagwnT2pg==} - engines: {node: '>=12.4.0'} - - '@nolyfill/hasown@1.0.44': - resolution: {integrity: sha512-GA/21lkTr2PAQuT6jGnhLuBD5IFd/AEhBXJ/tf33+/bVxPxg+5ejKx9jGQGnyV/P0eSmdup5E+s8b2HL6lOrwQ==} - engines: {node: '>=12.4.0'} - - '@nolyfill/is-core-module@1.0.39': - resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} - engines: {node: '>=12.4.0'} - - '@nolyfill/object-keys@1.0.44': - resolution: {integrity: sha512-k59MxUv8AFPd+GzZlGBhBaW6zu6t7f2euhctz7vdT3WsE3LVPANgE7uAN71IIpxaIn+bBEX1Xesf/rTwWqVO0Q==} - engines: {node: '>=12.4.0'} - - '@nolyfill/object.assign@1.0.44': - resolution: {integrity: sha512-cZoXq09YZXDgkxRMAP/TTb3kAsWm7p5OyBugWDe4fOfxf0XRI55mgDSkuyq41sV1qW1zVC5aSsKEh1hQo1KOvA==} - engines: {node: '>=12.4.0'} - - '@nolyfill/object.entries@1.0.44': - resolution: {integrity: sha512-RCxO6EH9YbvxQWGYLKOd7MjNi7vKzPkXv1VDWNsy1C8BksQxXNPQrddlu3INi1O2fexk82WXpCCeaCtpU/y21w==} - engines: {node: '>=12.4.0'} - - '@nolyfill/object.fromentries@1.0.44': - resolution: {integrity: sha512-/LrsCtpLmByZ6GwP/NeXULSgMyNsVr5d6FlgQy1HZatAiBc8c+WZ1VmFkK19ZLXCNNXBedXDultrp0x4Nz+QQw==} - engines: {node: '>=12.4.0'} - - '@nolyfill/object.groupby@1.0.44': - resolution: {integrity: sha512-jCt/8pN+10mlbeg0ZESpVVaqn5qqpv6kpjM+GDfEP7cXGDSPlIjtvfYWRZK4k4Gftkhhgqkzvcrr8z1wuNO1TQ==} - engines: {node: '>=12.4.0'} - - '@nolyfill/object.values@1.0.44': - resolution: {integrity: sha512-bwIpVzFMudUC0ofnvdSDB/OyGUizcU+r32ZZ0QTMbN03gUttMtdCFDekuSYT0XGFgufTQyZ4ONBnAeb3DFCPGQ==} - engines: {node: '>=12.4.0'} - - '@nolyfill/safe-regex-test@1.0.44': - resolution: {integrity: sha512-Q6veatd1NebtD8Sre6zjvO35QzG21IskMVOOEbePFcNO9noanNJgsqHeOCr0c5yZz6Z0DAizLg2gIZWokJSkXw==} - engines: {node: '>=12.4.0'} - - '@nolyfill/safer-buffer@1.0.44': - resolution: {integrity: sha512-Ouw1fMwjAy1V4MpnDASfu1DCPgkP0nNFteiiWbFoEGSqa7Vnmkb6if2c522N2WcMk+RuaaabQbC1F1D4/kTXcg==} - engines: {node: '>=12.4.0'} - - '@nolyfill/shared@1.0.44': - resolution: {integrity: sha512-NI1zxDh4LYL7PYlKKCwojjuc5CEZslywrOTKBNyodjmWjRiZ4AlCMs3Gp+zDoPQPNkYCSQp/luNojHmJWWfCbw==} - - '@nolyfill/string.prototype.includes@1.0.44': - resolution: {integrity: sha512-d1t7rnoAYyoap0X3a/gCnusCvxzK6v7uMFzW8k0mI2WtAK8HiKuzaQUwAriyVPh63GsvQCqvXx8Y5gtdh4LjSA==} - engines: {node: '>=12.4.0'} - - '@nolyfill/string.prototype.trimend@1.0.44': - resolution: {integrity: sha512-3dsKlf4Ma7o+uxLIg5OI1Tgwfet2pE8WTbPjEGWvOe6CSjMtK0skJnnSVHaEVX4N4mYU81To0qDeZOPqjaUotg==} - engines: {node: '>=12.4.0'} - - '@oxc-project/types@0.130.0': - resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==} + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} '@package-json/types@0.0.12': resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} - '@pkgr/core@0.2.9': - resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@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==} @@ -1106,8 +999,8 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@primer/octicons@19.26.0': - resolution: {integrity: sha512-K+ewEvXf9w+65i4OeLDXOhd1yT5qdMIpU/miqmjTuaGufZCxFn4131SIh0CfrinmLFnL9Ow60kk3hxx0Y4oIAQ==} + '@primer/octicons@19.28.0': + resolution: {integrity: sha512-FCpW9ZXI9U9h7wjYSXFQK4Zyp1Roc/kF8nymak4bYccWaWoUixbnIr4u8UYiRoPRSglm+23TZEyUZHrgNql9Jw==} '@replit/codemirror-indentation-markers@6.5.3': resolution: {integrity: sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw==} @@ -1157,97 +1050,97 @@ packages: resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} engines: {node: '>= 10'} - '@rolldown/binding-android-arm64@1.0.1': - resolution: {integrity: sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==} + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.1': - resolution: {integrity: sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==} + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.1': - resolution: {integrity: sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==} + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.1': - resolution: {integrity: sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==} + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.1': - resolution: {integrity: sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.1': - resolution: {integrity: sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==} + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.1': - resolution: {integrity: sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==} + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.1': - resolution: {integrity: sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==} + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.1': - resolution: {integrity: sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==} + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.1': - resolution: {integrity: sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==} + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.1': - resolution: {integrity: sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==} + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.1': - resolution: {integrity: sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==} + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.1': - resolution: {integrity: sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==} + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.1': - resolution: {integrity: sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==} + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.1': - resolution: {integrity: sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==} + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1306,8 +1199,8 @@ packages: peerDependencies: stylelint: ^17.6.0 - '@swc/helpers@0.5.21': - resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} '@tootallnate/once@2.0.1': resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==} @@ -1424,8 +1317,8 @@ packages: '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} @@ -1511,165 +1404,182 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.59.4': - resolution: {integrity: sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==} + '@typescript-eslint/eslint-plugin@8.60.1': + resolution: {integrity: sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.59.4 + '@typescript-eslint/parser': ^8.60.1 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.59.4': - resolution: {integrity: sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==} + '@typescript-eslint/parser@8.60.1': + resolution: {integrity: sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==} 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.59.4': - resolution: {integrity: sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==} + '@typescript-eslint/project-service@8.60.1': + resolution: {integrity: sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.59.4': - resolution: {integrity: sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==} + '@typescript-eslint/scope-manager@8.60.1': + resolution: {integrity: sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.59.4': - resolution: {integrity: sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==} + '@typescript-eslint/tsconfig-utils@8.60.1': + resolution: {integrity: sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.59.4': - resolution: {integrity: sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==} + '@typescript-eslint/type-utils@8.60.1': + resolution: {integrity: sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==} 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.59.4': - resolution: {integrity: sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==} + '@typescript-eslint/types@8.60.1': + resolution: {integrity: sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.59.4': - resolution: {integrity: sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==} + '@typescript-eslint/typescript-estree@8.60.1': + resolution: {integrity: sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.59.4': - resolution: {integrity: sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==} + '@typescript-eslint/utils@8.60.1': + resolution: {integrity: sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==} 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.59.4': - resolution: {integrity: sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==} + '@typescript-eslint/visitor-keys@8.60.1': + resolution: {integrity: sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} cpu: [arm] os: [android] - '@unrs/resolver-binding-android-arm64@1.11.1': - resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} cpu: [arm64] os: [android] - '@unrs/resolver-binding-darwin-arm64@1.11.1': - resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} cpu: [arm64] os: [darwin] - '@unrs/resolver-binding-darwin-x64@1.11.1': - resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} cpu: [x64] os: [darwin] - '@unrs/resolver-binding-freebsd-x64@1.11.1': - resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} cpu: [x64] os: [freebsd] - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} cpu: [arm] os: [linux] - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} cpu: [arm] os: [linux] - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} cpu: [arm64] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} cpu: [arm64] os: [linux] libc: [musl] - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} cpu: [ppc64] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} cpu: [riscv64] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} cpu: [riscv64] os: [linux] libc: [musl] - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} cpu: [s390x] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} cpu: [x64] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} cpu: [x64] os: [linux] libc: [musl] - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} cpu: [arm64] os: [win32] - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} cpu: [ia32] os: [win32] - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} cpu: [x64] os: [win32] @@ -1683,8 +1593,8 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.2.25 - '@vitest/eslint-plugin@1.6.17': - resolution: {integrity: sha512-sIVY9ZeVcXyPxFCNRkIt8Yw4keKIcUyp9/8qnmuomPwE+ST1htw5sZsbqdUMTiah9SmCg1JYoK9RqdDtPeNYYg==} + '@vitest/eslint-plugin@1.6.19': + resolution: {integrity: sha512-zodmXRsVKFsuHxHJILuTFaaKsrsxm0YsiOX65clk+LpCW9JrVXaf6ERXr0caDs+NEk0S62Jyk0K7XYQ7gWXheA==} engines: {node: '>=18'} peerDependencies: '@typescript-eslint/eslint-plugin': '*' @@ -1699,11 +1609,11 @@ packages: vitest: optional: true - '@vitest/expect@4.1.7': - resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} + '@vitest/expect@4.1.8': + resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} - '@vitest/mocker@4.1.7': - resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} + '@vitest/mocker@4.1.8': + resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1713,20 +1623,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.7': - resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} + '@vitest/pretty-format@4.1.8': + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} - '@vitest/runner@4.1.7': - resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} + '@vitest/runner@4.1.8': + resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} - '@vitest/snapshot@4.1.7': - resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} + '@vitest/snapshot@4.1.8': + resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} - '@vitest/spy@4.1.7': - resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} + '@vitest/spy@4.1.8': + resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} - '@vitest/utils@4.1.7': - resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} + '@vitest/utils@4.1.8': + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} '@volar/language-core@2.4.28': resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} @@ -1737,37 +1647,41 @@ packages: '@volar/typescript@2.4.28': resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} - '@vue/compiler-core@3.5.34': - resolution: {integrity: sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==} + '@vue/compiler-core@3.5.35': + resolution: {integrity: sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==} - '@vue/compiler-dom@3.5.34': - resolution: {integrity: sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==} + '@vue/compiler-dom@3.5.35': + resolution: {integrity: sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==} - '@vue/compiler-sfc@3.5.34': - resolution: {integrity: sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==} + '@vue/compiler-sfc@3.5.35': + resolution: {integrity: sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==} - '@vue/compiler-ssr@3.5.34': - resolution: {integrity: sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==} + '@vue/compiler-ssr@3.5.35': + resolution: {integrity: sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==} - '@vue/language-core@3.3.1': - resolution: {integrity: sha512-NP8g6V7x81NVOXbLupUvYY6i6LqUkjkVowe2epRedmpgaFCOdjgWHE/rQBvEJ4r7koAYODIjGeBWEdt6n7jYXQ==} + '@vue/language-core@3.3.3': + resolution: {integrity: sha512-X6p+7nfY7vVT6dQwUJ+v0Jfq/lwIfhL2jMi91dQ3ln4hnlGXlxsDu/FNkeyHYgvYtyQy18ZX76IZy7X4diDbiQ==} - '@vue/reactivity@3.5.34': - resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==} + '@vue/reactivity@3.5.35': + resolution: {integrity: sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==} - '@vue/runtime-core@3.5.34': - resolution: {integrity: sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==} + '@vue/runtime-core@3.5.35': + resolution: {integrity: sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==} - '@vue/runtime-dom@3.5.34': - resolution: {integrity: sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==} + '@vue/runtime-dom@3.5.35': + resolution: {integrity: sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==} - '@vue/server-renderer@3.5.34': - resolution: {integrity: sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==} + '@vue/server-renderer@3.5.35': + resolution: {integrity: sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==} peerDependencies: - vue: 3.5.34 + vue: 3.5.35 - '@vue/shared@3.5.34': - resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==} + '@vue/shared@3.5.35': + resolution: {integrity: sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==} + + abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} @@ -1799,6 +1713,14 @@ 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'} @@ -1807,6 +1729,10 @@ 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'} @@ -1835,9 +1761,40 @@ packages: 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'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + 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'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1849,11 +1806,25 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - axe-core@4.11.4: - resolution: {integrity: sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==} + available-typed-arrays@1.0.7: + 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: @@ -1870,23 +1841,33 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.27: - resolution: {integrity: sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==} + baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} engines: {node: '>=6.0.0'} hasBin: true + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + 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==} - brace-expansion@1.1.14: - resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -1905,16 +1886,28 @@ packages: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} - builtin-modules@5.1.0: - resolution: {integrity: sha512-c5JxaDrzwRjq3WyJkI1AGR5xy6Gr6udlt7sQPbl09+3ckB+Zo2qqQ2KhCTBr7Q8dHB43bENGYEk4xddrFH/b7A==} + builtin-modules@5.2.0: + resolution: {integrity: sha512-02yxLeyxF4dNl6SlY6/5HfRSrSdZ/sCPoxy2kZNP5dZZX8LSAD9aE2gtJIUgWrsQTiMPl3mxESyrobSwvRGisQ==} engines: {node: '>=18.20'} bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - cacheable@2.3.4: - resolution: {integrity: sha512-djgxybDbw9fL/ZWMI3+CE8ZilNxcwFkVtDc1gJ+IlOSSWkSMPQabhV/XCHTQ6pwwN6aivXPZ43omTooZiX06Ew==} + cacheable@2.3.5: + resolution: {integrity: sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} @@ -1924,13 +1917,20 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001791: - resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} 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,9 +1985,20 @@ packages: 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'} + codemirror-lang-elixir@4.0.1: resolution: {integrity: sha512-z6W/XB4b7TZrp9EZYBGVq93vQfvKbff+1iM8YZaVErL0dguBAeLmVRlEv1NuDZHOP1qjJ3NwyibkUkNWn7q9VQ==} @@ -2019,6 +2030,9 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -2031,8 +2045,8 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - comment-parser@1.4.6: - resolution: {integrity: sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==} + comment-parser@1.4.7: + resolution: {integrity: sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ==} engines: {node: '>= 12.0.0'} compare-versions@6.1.1: @@ -2041,18 +2055,22 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - 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==} @@ -2129,8 +2147,8 @@ packages: peerDependencies: cytoscape: ^3.2.0 - cytoscape@3.33.3: - resolution: {integrity: sha512-Gej7U+OKR+LZ8kvX7rb2HhCYJ0IhvEFsnkud4SB1PR+BUY/TsSO0dmOW59WEVLu51b1Rm+gQRKoz4bLYxGSZ2g==} + cytoscape@3.33.4: + resolution: {integrity: sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww==} engines: {node: '>=0.10'} d3-array@2.12.1: @@ -2278,12 +2296,28 @@ packages: 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'} - dayjs@1.11.20: - resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} @@ -2319,6 +2353,14 @@ packages: 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'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + delaunator@5.1.0: resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} @@ -2365,17 +2407,30 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.4.2: - resolution: {integrity: sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==} + dompurify@3.4.7: + resolution: {integrity: sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dunder-proto@1.0.1: + 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==} - electron-to-chromium@1.5.349: - resolution: {integrity: sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==} + 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==} elkjs@0.9.3: resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} @@ -2405,6 +2460,14 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} @@ -2412,8 +2475,24 @@ packages: es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} - es-toolkit@1.46.1: - resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + es-toolkit@1.47.0: + resolution: {integrity: sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==} esbuild@0.28.0: resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} @@ -2459,8 +2538,8 @@ packages: eslint-import-resolver-node@0.3.10: resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} - eslint-import-resolver-typescript@4.4.4: - resolution: {integrity: sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==} + eslint-import-resolver-typescript@4.4.5: + resolution: {integrity: sha512-nbE5XLph6TLtGYcu/U6e6ZVXyKBhbDWK5cLGk76eJ7NdZpwf1P9EFkpt1Z01mNZNrrilsAYWKH6zUkL4reoXbw==} engines: {node: ^16.17.0 || >=18.6.0} peerDependencies: eslint: '*' @@ -2472,8 +2551,8 @@ packages: eslint-plugin-import-x: optional: true - eslint-module-utils@2.12.1: - resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + eslint-module-utils@2.13.0: + resolution: {integrity: sha512-bLohSkT6469rRs8czj0tLTD8vaeIS/whvPRJVjDr7IuoTT1k5DYDERlNycjDj/HkOlvQdYurmfZ/g3fG5bgeLQ==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -2571,8 +2650,8 @@ packages: peerDependencies: eslint: '>=8.40.0' - eslint-plugin-prettier@5.5.5: - resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} + 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' @@ -2602,8 +2681,8 @@ packages: peerDependencies: eslint: '>=9.38.0' - eslint-plugin-vue-scoped-css@3.1.0: - resolution: {integrity: sha512-R9XLrIZaP6QGz9b4kO2K4+lP4NcO2TKcw71zBtIYCoqqTk5ja1ySruYAllBT2LPIJVQ4NZaB2IFSvLjLEpYqQA==} + eslint-plugin-vue-scoped-css@3.1.1: + resolution: {integrity: sha512-GIskMvLPnDtiu88rWXQHy2b2QZ4j959N5UgghML64jH0sg3Km+HRa9m7nkpcEBGLD4iA4vtMDbBIoLdFcbT8lQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} peerDependencies: eslint: '>=9.38.0' @@ -2616,8 +2695,8 @@ packages: postcss-styl: optional: true - eslint-plugin-vue@10.9.1: - resolution: {integrity: sha512-cHB0Tf4Duvzwecwd/AqWzZvF/QszE13BhjVUpVXWCy9AeMR5GjkAjP3i85vqgLgOuTmkHR1OJ5oMeqLHtuw8zg==} + eslint-plugin-vue@10.9.2: + resolution: {integrity: sha512-4g7ZP3pYcuqd7Zp0pzUKcos0W+RkjBz4EGdhJ92FcYk6v03Ti/GK5NwjgsjxHK+98eXDbHeK7VtX1az7/8doZA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -2655,8 +2734,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.4.0: - resolution: {integrity: sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==} + eslint@10.4.1: + resolution: {integrity: sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -2707,10 +2786,21 @@ packages: 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==} @@ -2727,8 +2817,8 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} fastest-levenshtein@1.0.16: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} @@ -2752,6 +2842,10 @@ 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==} @@ -2781,10 +2875,31 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + for-each@0.3.5: + 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} @@ -2795,16 +2910,45 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + functional-red-black-tree@1.0.1: resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} - get-east-asian-width@1.5.0: - resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + 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'} @@ -2813,6 +2957,10 @@ 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'} @@ -2833,6 +2981,10 @@ packages: resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} engines: {node: '>=18'} + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + globby@16.2.0: resolution: {integrity: sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==} engines: {node: '>=20'} @@ -2840,6 +2992,10 @@ packages: globjoin@0.1.4: resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -2854,6 +3010,23 @@ packages: resolution: {integrity: sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==} 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'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -2862,10 +3035,29 @@ packages: resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} engines: {node: '>=12'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hashery@1.5.1: resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} engines: {node: '>=20'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + hookified@1.15.1: resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} @@ -2887,6 +3079,10 @@ 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'} @@ -2924,6 +3120,13 @@ 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==} @@ -2931,6 +3134,16 @@ 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'} + internmap@1.0.1: resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} @@ -2944,13 +3157,29 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + is-buffer@1.1.6: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} @@ -2961,6 +3190,22 @@ packages: is-bun-module@2.0.0: resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -2968,10 +3213,22 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-finalizationregistry@1.1.1: + 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'} + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2979,6 +3236,18 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2990,12 +3259,57 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + 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==} + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + 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} @@ -3041,6 +3355,13 @@ packages: 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==} + jsdoc-type-pratt-parser@7.2.0: resolution: {integrity: sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==} engines: {node: '>=20.0.0'} @@ -3071,9 +3392,15 @@ 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 @@ -3081,10 +3408,17 @@ 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} @@ -3093,10 +3427,17 @@ packages: 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 + katex@0.17.0: + resolution: {integrity: sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -3114,6 +3455,9 @@ packages: 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==} @@ -3215,8 +3559,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + linkify-it@5.0.1: + resolution: {integrity: sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==} locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} @@ -3243,6 +3587,12 @@ packages: 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==} @@ -3269,10 +3619,14 @@ packages: engines: {node: '>= 12'} hasBin: true - material-icon-theme@5.34.0: - resolution: {integrity: sha512-m+ZgtdtJMkfOwxpfUH8FqyJbfnAJuxMBNv4XRvz3m808Is42RbeYc/5W0bk5qGJJLxzF5Cupj6Or52aQISwz3A==} + material-icon-theme@5.35.0: + resolution: {integrity: sha512-ptU3rjmZjPCeLRog0HcJ1HtnoVgOslc350WHk1oIvX7fVFE4NBAF7GL3QvCCEjtd1TSSDoxmS4dWsv6bTB1x9g==} engines: {vscode: ^1.55.0} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mathml-tag-names@4.0.0: resolution: {integrity: sha512-aa6AU2Pcx0VP/XWnh8IGL0SYSgQHDT6Ucror2j2mXeFAlN3ahaNs8EZtG1YiticMkSLj3Gt6VPFfZogt7G5iFQ==} @@ -3393,9 +3747,6 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - mlly@1.8.2: - resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - moo@0.5.3: resolution: {integrity: sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==} @@ -3405,6 +3756,9 @@ 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==} @@ -3446,13 +3800,9 @@ packages: encoding: optional: true - node-releases@2.0.38: - resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} - - nolyfill@1.0.44: - resolution: {integrity: sha512-PoggwVLiJUn0MnodpftsiC7EuknW5+6v62ntTOQ6T6l7g2r6aoaOwgk0tQW2BxGLYw9bF298LL8jDFTmEFuzlA==} - engines: {node: '>=12.4.0'} - hasBin: true + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -3461,9 +3811,16 @@ 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'} @@ -3472,9 +3829,44 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + 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==} @@ -3482,6 +3874,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-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -3517,6 +3917,10 @@ 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'} @@ -3533,6 +3937,9 @@ 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==} @@ -3552,9 +3959,6 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - playwright-core@1.60.0: resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} engines: {node: '>=18'} @@ -3575,6 +3979,10 @@ packages: points-on-path@0.2.1: resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss-html@1.8.1: resolution: {integrity: sha512-OLF6P7qctfAWayOhLpcVnTGqVeJzu2W3WpIYelfz2+JV5oGxfkcEvweN9U4XpeqE0P98dcD9ssusGwlF0TK0uQ==} engines: {node: ^12 || >=14} @@ -3670,10 +4078,14 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qified@0.9.1: - resolution: {integrity: sha512-n7mar4T0xQ+39dE2vGTAlbxUEpndwPANH0kDef1/MYsB8Bba9wshkybIRx74qgcvKQPEWErf9AqAdYjhzY2Ilg==} + qified@0.10.1: + 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==} @@ -3690,10 +4102,20 @@ 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} + reflect.getprototypeof@1.0.10: + 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} @@ -3702,6 +4124,10 @@ packages: 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==} hasBin: true @@ -3710,6 +4136,16 @@ packages: 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'} @@ -3729,31 +4165,43 @@ packages: engines: {node: '>= 0.4'} hasBin: true - resolve@2.0.0-next.6: - resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} 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==} - rolldown-license-plugin@3.0.7: - resolution: {integrity: sha512-jBgDoLE/UWPB1VWzERbpWGuHA0+YBDflFHX889Mayg4Z8Re27kFh7GKbuaEt4f3VK1xpJ4PGVcUoT3V2+fAybw==} + rolldown-license-plugin@3.0.9: + resolution: {integrity: sha512-40u0paM+f049toEj+/q8PlIQXbgkwPSqORtRJihoXr/v1V4amhVSi2uWOZSWyVp1+V/vkTyAV+Ib/fA+DeO3Ag==} peerDependencies: rolldown: '*' - rolldown@1.0.1: - resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==} + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} 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 @@ -3764,6 +4212,27 @@ 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'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.6.0: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} @@ -3780,21 +4249,33 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.8.0: - resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} engines: {node: '>=10'} hasBin: true - seroval-plugins@1.5.3: - resolution: {integrity: sha512-LhVh4KjjkKmCxOUjoaUwtqbDjyMfnA535yEmmGDuwZcIYtw8ns6tZmeszNTECeUg/3sJpnEjsz/KhQrcPXPw1Q==} + seroval-plugins@1.5.4: + resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==} engines: {node: '>=10'} peerDependencies: seroval: ^1.0 - seroval@1.5.3: - resolution: {integrity: sha512-BXe0x4buEeYiIKaRUnth1WqCILQ3k4O67KP/B4pC3pVz0Mv2c96ngA9QDREUYxWY1sb2RZVRqwI9RcpVMyHCVw==} + seroval@1.5.4: + resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==} engines: {node: '>=10'} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3803,6 +4284,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -3826,8 +4323,8 @@ packages: resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} - solid-js@1.9.12: - resolution: {integrity: sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==} + solid-js@1.9.13: + resolution: {integrity: sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ==} solid-transition-group@0.2.3: resolution: {integrity: sha512-iB72c9N5Kz9ykRqIXl0lQohOau4t0dhel9kjwFvx81UZJbVwaChMuBuyhiZmK24b8aKEK0w3uFM96ZxzcyZGdg==} @@ -3851,6 +4348,11 @@ packages: 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'} @@ -3865,6 +4367,14 @@ packages: std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + stop-iteration-iterator@1.1.0: + 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'} @@ -3877,6 +4387,26 @@ packages: 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'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + 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'} @@ -3944,6 +4474,10 @@ packages: 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'} @@ -3980,8 +4514,8 @@ packages: resolution: {integrity: sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==} engines: {node: '>=14'} - synckit@0.11.12: - resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} engines: {node: ^14.18.0 || >=16.0.0} table@6.9.0: @@ -4007,15 +4541,18 @@ packages: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.1.2: - resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} tinyrainbow@3.1.0: @@ -4032,6 +4569,10 @@ 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'} @@ -4065,6 +4606,12 @@ 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'} @@ -4073,8 +4620,24 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} - typescript-eslint@8.59.4: - resolution: {integrity: sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.60.1: + resolution: {integrity: sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -4090,18 +4653,19 @@ packages: engines: {node: '>=14.17'} hasBin: true - typo-js@1.3.1: - resolution: {integrity: sha512-elJkpCL6Z77Ghw0Lv0lGnhBAjSTOQ5FhiVOCfOuxhaoTT2xtLVbqikYItK5HHchzPbHEUFAcjOH669T2ZzeCbg==} + typo-js@1.3.2: + resolution: {integrity: sha512-Z1YkJ7IIYNrFeOxAlHUercY4Q2I+PhYD/3VkWpJGy/Oqudy3bFpNcQxnv6Oa9fTSXCHPGz1eDoX1bZYm2Z891A==} uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - ufo@1.6.4: - resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} - uint8-to-base64@0.2.1: resolution: {integrity: sha512-uO/84GaoDUfiAxpa8EksjVLE77A9Kc7ZTziN4zRpq4de9yLaLcZn3jx1/sVjyupsywcVX6RKWbqLe7gUNyzH+Q==} + unbox-primitive@1.1.0: + 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==} @@ -4113,8 +4677,12 @@ packages: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} - unrs-resolver@1.11.1: - resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + 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==} @@ -4122,8 +4690,8 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - updates@17.16.13: - resolution: {integrity: sha512-Od8Ea50dJZWp3dKnibQSQ08Wp1Z3qhn3i0zMBOB47qPltSlGjZFVDR+XjfSEzQIE/Grp2z8OQ/G4YuNiH3X8xA==} + updates@17.17.3: + resolution: {integrity: sha512-ZIhWarBUBmKG65d0AeOOMlZFonGWn6Ntol4/epga/xbQymEOh/2s07U+1UGM94y9JEPbk4CjowYgEo3F76ZxYA==} engines: {node: '>=22'} hasBin: true @@ -4133,23 +4701,36 @@ 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==} - uuid@11.1.1: - resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + uuid@14.0.0: + 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==} peerDependencies: vite: '*' - vite@8.0.13: - resolution: {integrity: sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==} + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4191,20 +4772,20 @@ packages: yaml: optional: true - vitest@4.1.7: - resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} + vitest@4.1.8: + resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} 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.7 - '@vitest/browser-preview': 4.1.7 - '@vitest/browser-webdriverio': 4.1.7 - '@vitest/coverage-istanbul': 4.1.7 - '@vitest/coverage-v8': 4.1.7 - '@vitest/ui': 4.1.7 + '@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 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -4250,14 +4831,14 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - vue-tsc@3.3.1: - resolution: {integrity: sha512-webBP3jhlxzhELZ2g+11KJ6pg5OVY1xWhWrj7N/yQMi1CrtxJnW+tUACyRVeDK0cQNLP2Va5HNYK8pe+7c+msw==} + vue-tsc@3.3.3: + resolution: {integrity: sha512-SWUEG7YRUeDJHT7Xsuhf02elYX2gxPzzAII7OxDAh4KNOr4QHQ0Lls0YfnaO5GNd560CwVa2HTfdqmA5MqvRqQ==} hasBin: true peerDependencies: typescript: '>=5.0.0' - vue@3.5.34: - resolution: {integrity: sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==} + vue@3.5.35: + resolution: {integrity: sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -4294,6 +4875,22 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.21: + resolution: {integrity: sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw==} + engines: {node: '>= 0.4'} + which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -4312,12 +4909,15 @@ 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} - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -4352,32 +4952,32 @@ snapshots: '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.6.0 - tinyexec: 1.1.2 + tinyexec: 1.2.4 - '@babel/code-frame@7.29.0': + '@babel/code-frame@7.29.7': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} - '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} - '@babel/parser@7.29.3': + '@babel/parser@7.29.7': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 - '@babel/runtime@7.29.2': {} + '@babel/runtime@7.29.7': {} - '@babel/types@7.29.0': + '@babel/types@7.29.7': dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 '@braintree/sanitize-url@7.1.2': {} - '@cacheable/memory@2.0.8': + '@cacheable/memory@2.0.9': dependencies: '@cacheable/utils': 2.4.1 '@keyv/bigmap': 1.3.1(keyv@5.6.0) @@ -4442,14 +5042,14 @@ snapshots: '@citation-js/plugin-yaml@0.6.2': dependencies: - js-yaml: 4.1.1 + js-yaml: 4.2.0 '@citation-js/plugin-zenodo@0.6.2': dependencies: '@citation-js/date': 0.5.1 '@citation-js/name': 0.4.2 - '@codemirror/autocomplete@6.20.2': + '@codemirror/autocomplete@6.20.3': dependencies: '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 @@ -4475,11 +5075,11 @@ snapshots: '@codemirror/lang-cpp@6.0.3': dependencies: '@codemirror/language': 6.12.3 - '@lezer/cpp': 1.1.5 + '@lezer/cpp': 1.1.6 '@codemirror/lang-css@6.3.1': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 '@lezer/common': 1.5.2 @@ -4487,7 +5087,7 @@ snapshots: '@codemirror/lang-go@6.0.1': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 '@lezer/common': 1.5.2 @@ -4495,7 +5095,7 @@ snapshots: '@codemirror/lang-html@6.4.11': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/lang-css': 6.3.1 '@codemirror/lang-javascript': 6.2.5 '@codemirror/language': 6.12.3 @@ -4512,7 +5112,7 @@ snapshots: '@codemirror/lang-javascript@6.2.5': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/language': 6.12.3 '@codemirror/lint': 6.9.6 '@codemirror/state': 6.6.0 @@ -4522,7 +5122,7 @@ snapshots: '@codemirror/lang-jinja@6.0.1': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/lang-html': 6.4.11 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 @@ -4546,7 +5146,7 @@ snapshots: '@codemirror/lang-liquid@6.3.2': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/lang-html': 6.4.11 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 @@ -4557,13 +5157,13 @@ snapshots: '@codemirror/lang-markdown@6.5.0': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@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.0 '@lezer/common': 1.5.2 - '@lezer/markdown': 1.6.3 + '@lezer/markdown': 1.6.4 '@codemirror/lang-php@6.0.2': dependencies: @@ -4575,11 +5175,11 @@ snapshots: '@codemirror/lang-python@6.2.1': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 '@lezer/common': 1.5.2 - '@lezer/python': 1.1.18 + '@lezer/python': 1.1.19 '@codemirror/lang-rust@6.0.2': dependencies: @@ -4596,7 +5196,7 @@ snapshots: '@codemirror/lang-sql@6.10.0': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 '@lezer/common': 1.5.2 @@ -4621,7 +5221,7 @@ snapshots: '@codemirror/lang-xml@6.1.0': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 '@codemirror/view': 6.43.0 @@ -4630,7 +5230,7 @@ snapshots: '@codemirror/lang-yaml@6.1.3': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 '@lezer/common': 1.5.2 @@ -4700,7 +5300,7 @@ snapshots: style-mod: 4.1.3 w3c-keyname: 2.2.8 - '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@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)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 @@ -4709,7 +5309,7 @@ snapshots: dependencies: '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + '@csstools/css-syntax-patches-for-csstree@1.1.4(css-tree@3.2.1)': optionalDependencies: css-tree: 3.2.1 @@ -4730,7 +5330,7 @@ snapshots: '@deltablot/dropzone@7.4.3': dependencies: - '@swc/helpers': 0.5.21 + '@swc/helpers': 0.5.23 '@emnapi/core@1.10.0': dependencies: @@ -4826,24 +5426,24 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.4.0(jiti@2.7.0))': + '@eslint-community/eslint-plugin-eslint-comments@4.7.2(eslint@10.4.1(jiti@2.7.0))': dependencies: escape-string-regexp: 4.0.0 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) ignore: 7.0.5 - '@eslint-community/eslint-utils@4.9.1(eslint@10.4.0(jiti@2.7.0))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.4.1(jiti@2.7.0))': dependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@1.4.1(eslint@10.4.0(jiti@2.7.0))': + '@eslint/compat@1.4.1(eslint@10.4.1(jiti@2.7.0))': dependencies: '@eslint/core': 0.17.0 optionalDependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) '@eslint/config-array@0.23.5': dependencies: @@ -4873,7 +5473,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.2.0 minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -4881,21 +5481,16 @@ snapshots: '@eslint/js@9.39.4': {} - '@eslint/json@1.2.0': + '@eslint/json@2.0.0': dependencies: '@eslint/core': 1.2.1 - '@eslint/plugin-kit': 0.6.1 + '@eslint/plugin-kit': 0.7.2 '@humanwhocodes/momoa': 3.3.10 natural-compare: 1.4.0 '@eslint/object-schema@3.0.5': {} - '@eslint/plugin-kit@0.6.1': - dependencies: - '@eslint/core': 1.2.1 - levn: 0.4.1 - - '@eslint/plugin-kit@0.7.1': + '@eslint/plugin-kit@0.7.2': dependencies: '@eslint/core': 1.2.1 levn: 0.4.1 @@ -4933,11 +5528,11 @@ snapshots: '@iconify/types@2.0.0': {} - '@iconify/utils@3.1.1': + '@iconify/utils@3.1.3': dependencies: '@antfu/install-pkg': 1.1.0 '@iconify/types': 2.0.0 - mlly: 1.8.2 + import-meta-resolve: 4.2.0 '@jest/environment@29.7.0': dependencies: @@ -4994,7 +5589,7 @@ snapshots: '@lezer/common@1.5.2': {} - '@lezer/cpp@1.1.5': + '@lezer/cpp@1.1.6': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 @@ -5044,7 +5639,7 @@ snapshots: dependencies: '@lezer/common': 1.5.2 - '@lezer/markdown@1.6.3': + '@lezer/markdown@1.6.4': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 @@ -5055,7 +5650,7 @@ snapshots: '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 - '@lezer/python@1.1.18': + '@lezer/python@1.1.19': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 @@ -5109,13 +5704,6 @@ snapshots: dependencies: '@chevrotain/types': 11.1.2 - '@napi-rs/wasm-runtime@0.2.12': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 - optional: true - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -5135,73 +5723,11 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@nolyfill/abab@1.0.44': {} - - '@nolyfill/array-includes@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/array.prototype.findlastindex@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/array.prototype.flat@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/array.prototype.flatmap@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/es-set-tostringtag@1.0.44': {} - - '@nolyfill/hasown@1.0.44': {} - - '@nolyfill/is-core-module@1.0.39': {} - - '@nolyfill/object-keys@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/object.assign@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/object.entries@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/object.fromentries@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/object.groupby@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/object.values@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/safe-regex-test@1.0.44': {} - - '@nolyfill/safer-buffer@1.0.44': {} - - '@nolyfill/shared@1.0.44': {} - - '@nolyfill/string.prototype.includes@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/string.prototype.trimend@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@oxc-project/types@0.130.0': {} + '@oxc-project/types@0.133.0': {} '@package-json/types@0.0.12': {} - '@pkgr/core@0.2.9': {} + '@pkgr/core@0.3.6': {} '@playwright/test@1.60.0': dependencies: @@ -5209,7 +5735,7 @@ snapshots: '@popperjs/core@2.11.8': {} - '@primer/octicons@19.26.0': + '@primer/octicons@19.28.0': dependencies: object-assign: 4.1.1 @@ -5219,9 +5745,9 @@ snapshots: '@codemirror/state': 6.6.0 '@codemirror/view': 6.43.0 - '@replit/codemirror-lang-nix@6.0.1(@codemirror/autocomplete@6.20.2)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@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.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.10)': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 '@codemirror/view': 6.43.0 @@ -5229,9 +5755,9 @@ snapshots: '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 - '@replit/codemirror-lang-svelte@6.0.0(@codemirror/autocomplete@6.20.2)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@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.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.10)': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/lang-css': 6.3.1 '@codemirror/lang-html': 6.4.11 '@codemirror/lang-javascript': 6.2.5 @@ -5243,9 +5769,9 @@ snapshots: '@lezer/javascript': 1.5.4 '@lezer/lr': 1.4.10 - '@replit/codemirror-vscode-keymap@6.0.2(@codemirror/autocomplete@6.20.2)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)': + '@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.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/commands': 6.10.3 '@codemirror/language': 6.12.3 '@codemirror/lint': 6.9.6 @@ -5255,53 +5781,53 @@ snapshots: '@resvg/resvg-wasm@2.6.2': {} - '@rolldown/binding-android-arm64@1.0.1': + '@rolldown/binding-android-arm64@1.0.3': optional: true - '@rolldown/binding-darwin-arm64@1.0.1': + '@rolldown/binding-darwin-arm64@1.0.3': optional: true - '@rolldown/binding-darwin-x64@1.0.1': + '@rolldown/binding-darwin-x64@1.0.3': optional: true - '@rolldown/binding-freebsd-x64@1.0.1': + '@rolldown/binding-freebsd-x64@1.0.3': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.1': + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.1': + '@rolldown/binding-linux-arm64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.1': + '@rolldown/binding-linux-arm64-musl@1.0.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.1': + '@rolldown/binding-linux-ppc64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.1': + '@rolldown/binding-linux-s390x-gnu@1.0.3': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.1': + '@rolldown/binding-linux-x64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-x64-musl@1.0.1': + '@rolldown/binding-linux-x64-musl@1.0.3': optional: true - '@rolldown/binding-openharmony-arm64@1.0.1': + '@rolldown/binding-openharmony-arm64@1.0.3': optional: true - '@rolldown/binding-wasm32-wasi@1.0.1': + '@rolldown/binding-wasm32-wasi@1.0.3': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.1': + '@rolldown/binding-win32-arm64-msvc@1.0.3': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.1': + '@rolldown/binding-win32-x64-msvc@1.0.3': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -5327,26 +5853,26 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@solid-primitives/refs@1.1.3(solid-js@1.9.12)': + '@solid-primitives/refs@1.1.3(solid-js@1.9.13)': dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@1.9.12) - solid-js: 1.9.12 + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 - '@solid-primitives/transition-group@1.1.2(solid-js@1.9.12)': + '@solid-primitives/transition-group@1.1.2(solid-js@1.9.13)': dependencies: - solid-js: 1.9.12 + solid-js: 1.9.13 - '@solid-primitives/utils@6.4.0(solid-js@1.9.12)': + '@solid-primitives/utils@6.4.0(solid-js@1.9.13)': dependencies: - solid-js: 1.9.12 + solid-js: 1.9.13 '@standard-schema/spec@1.1.0': {} - '@stylistic/eslint-plugin@5.10.0(eslint@10.4.0(jiti@2.7.0))': + '@stylistic/eslint-plugin@5.10.0(eslint@10.4.1(jiti@2.7.0))': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) - '@typescript-eslint/types': 8.59.4 - eslint: 10.4.0(jiti@2.7.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + '@typescript-eslint/types': 8.60.1 + eslint: 10.4.1(jiti@2.7.0) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 @@ -5363,7 +5889,7 @@ snapshots: style-search: 0.1.0 stylelint: 17.12.0(typescript@6.0.3) - '@swc/helpers@0.5.21': + '@swc/helpers@0.5.23': dependencies: tslib: 2.8.1 @@ -5508,7 +6034,7 @@ snapshots: '@types/esrecurse@4.3.1': {} - '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} '@types/geojson@7946.0.16': {} @@ -5558,7 +6084,7 @@ snapshots: '@types/tern@0.23.9': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/throttle-debounce@5.0.2': {} @@ -5583,15 +6109,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.59.4 - '@typescript-eslint/type-utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.59.4 - eslint: 10.4.0(jiti@2.7.0) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/type-utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.60.1 + 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) @@ -5599,15 +6125,31 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.59.4 - '@typescript-eslint/type-utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.59.4 - eslint: 10.4.0(jiti@2.7.0) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/type-utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.60.1 + 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.60.1(@typescript-eslint/parser@8.60.1(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.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/type-utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.60.1 + eslint: 10.4.1(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -5615,201 +6157,212 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.59.4 - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.59.4 + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.60.1 debug: 4.4.3 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.59.4 - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.59.4 + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.60.1 debug: 4.4.3 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.59.4(typescript@5.9.3)': + '@typescript-eslint/project-service@8.60.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@5.9.3) - '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3) + '@typescript-eslint/types': 8.60.1 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.59.4(typescript@6.0.3)': + '@typescript-eslint/project-service@8.60.1(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@6.0.3) - '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@6.0.3) + '@typescript-eslint/types': 8.60.1 debug: 4.4.3 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.59.4': + '@typescript-eslint/scope-manager@8.60.1': dependencies: - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/visitor-keys': 8.59.4 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 - '@typescript-eslint/tsconfig-utils@8.59.4(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.60.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.59.4(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.60.1(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) debug: 4.4.3 - eslint: 10.4.0(jiti@2.7.0) + 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.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) - '@typescript-eslint/utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) debug: 4.4.3 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(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.59.4': {} + '@typescript-eslint/types@8.60.1': {} - '@typescript-eslint/typescript-estree@8.59.4(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.60.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.59.4(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@5.9.3) - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/visitor-keys': 8.59.4 + '@typescript-eslint/project-service': 8.60.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3) + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.8.0 - tinyglobby: 0.2.16 + 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.59.4(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.60.1(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.59.4(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@6.0.3) - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/visitor-keys': 8.59.4 + '@typescript-eslint/project-service': 8.60.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@6.0.3) + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.8.0 - tinyglobby: 0.2.16 + semver: 7.8.1 + 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.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.59.4 - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3) - eslint: 10.4.0(jiti@2.7.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) + eslint: 10.4.1(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.59.4 - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) - eslint: 10.4.0(jiti@2.7.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + eslint: 10.4.1(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.59.4': + '@typescript-eslint/visitor-keys@8.60.1': dependencies: - '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/types': 8.60.1 eslint-visitor-keys: 5.0.1 - '@unrs/resolver-binding-android-arm-eabi@1.11.1': + '@unrs/resolver-binding-android-arm-eabi@1.12.2': optional: true - '@unrs/resolver-binding-android-arm64@1.11.1': + '@unrs/resolver-binding-android-arm64@1.12.2': optional: true - '@unrs/resolver-binding-darwin-arm64@1.11.1': + '@unrs/resolver-binding-darwin-arm64@1.12.2': optional: true - '@unrs/resolver-binding-darwin-x64@1.11.1': + '@unrs/resolver-binding-darwin-x64@1.12.2': optional: true - '@unrs/resolver-binding-freebsd-x64@1.11.1': + '@unrs/resolver-binding-freebsd-x64@1.12.2': optional: true - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': optional: true - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': optional: true - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': optional: true - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': optional: true - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': optional: true - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': optional: true - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': optional: true - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': optional: true - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': optional: true - '@unrs/resolver-binding-linux-x64-musl@1.11.1': + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': optional: true - '@unrs/resolver-binding-wasm32-wasi@1.11.1': + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': dependencies: - '@napi-rs/wasm-runtime': 0.2.12 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': optional: true - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': optional: true - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true '@upsetjs/venn.js@2.0.0': @@ -5817,62 +6370,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.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.34(typescript@6.0.3))': + '@vitejs/plugin-vue@6.0.7(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) - vue: 3.5.34(typescript@6.0.3) + vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + vue: 3.5.35(typescript@6.0.3) - '@vitest/eslint-plugin@1.6.17(@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)))': + '@vitest/eslint-plugin@1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(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.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)))': dependencies: - '@typescript-eslint/scope-manager': 8.59.4 - '@typescript-eslint/utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.4.0(jiti@2.7.0) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.4.1(jiti@2.7.0) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(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: 6.0.3 - vitest: 4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + vitest: 4.1.8(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) transitivePeerDependencies: - supports-color - '@vitest/expect@4.1.7': + '@vitest/expect@4.1.8': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.7 - '@vitest/utils': 4.1.7 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.7(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))': + '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))': dependencies: - '@vitest/spy': 4.1.7 + '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) - '@vitest/pretty-format@4.1.7': + '@vitest/pretty-format@4.1.8': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.7': + '@vitest/runner@4.1.8': dependencies: - '@vitest/utils': 4.1.7 + '@vitest/utils': 4.1.8 pathe: 2.0.3 - '@vitest/snapshot@4.1.7': + '@vitest/snapshot@4.1.8': dependencies: - '@vitest/pretty-format': 4.1.7 - '@vitest/utils': 4.1.7 + '@vitest/pretty-format': 4.1.8 + '@vitest/utils': 4.1.8 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.7': {} + '@vitest/spy@4.1.8': {} - '@vitest/utils@4.1.7': + '@vitest/utils@4.1.8': dependencies: - '@vitest/pretty-format': 4.1.7 + '@vitest/pretty-format': 4.1.8 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -5888,69 +6441,71 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.1.0 - '@vue/compiler-core@3.5.34': + '@vue/compiler-core@3.5.35': dependencies: - '@babel/parser': 7.29.3 - '@vue/shared': 3.5.34 + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.35 entities: 7.0.1 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.34': + '@vue/compiler-dom@3.5.35': dependencies: - '@vue/compiler-core': 3.5.34 - '@vue/shared': 3.5.34 + '@vue/compiler-core': 3.5.35 + '@vue/shared': 3.5.35 - '@vue/compiler-sfc@3.5.34': + '@vue/compiler-sfc@3.5.35': dependencies: - '@babel/parser': 7.29.3 - '@vue/compiler-core': 3.5.34 - '@vue/compiler-dom': 3.5.34 - '@vue/compiler-ssr': 3.5.34 - '@vue/shared': 3.5.34 + '@babel/parser': 7.29.7 + '@vue/compiler-core': 3.5.35 + '@vue/compiler-dom': 3.5.35 + '@vue/compiler-ssr': 3.5.35 + '@vue/shared': 3.5.35 estree-walker: 2.0.2 magic-string: 0.30.21 postcss: 8.5.15 source-map-js: 1.2.1 - '@vue/compiler-ssr@3.5.34': + '@vue/compiler-ssr@3.5.35': dependencies: - '@vue/compiler-dom': 3.5.34 - '@vue/shared': 3.5.34 + '@vue/compiler-dom': 3.5.35 + '@vue/shared': 3.5.35 - '@vue/language-core@3.3.1': + '@vue/language-core@3.3.3': dependencies: '@volar/language-core': 2.4.28 - '@vue/compiler-dom': 3.5.34 - '@vue/shared': 3.5.34 + '@vue/compiler-dom': 3.5.35 + '@vue/shared': 3.5.35 alien-signals: 3.2.1 muggle-string: 0.4.1 path-browserify: 1.0.1 picomatch: 4.0.4 - '@vue/reactivity@3.5.34': + '@vue/reactivity@3.5.35': dependencies: - '@vue/shared': 3.5.34 + '@vue/shared': 3.5.35 - '@vue/runtime-core@3.5.34': + '@vue/runtime-core@3.5.35': dependencies: - '@vue/reactivity': 3.5.34 - '@vue/shared': 3.5.34 + '@vue/reactivity': 3.5.35 + '@vue/shared': 3.5.35 - '@vue/runtime-dom@3.5.34': + '@vue/runtime-dom@3.5.35': dependencies: - '@vue/reactivity': 3.5.34 - '@vue/runtime-core': 3.5.34 - '@vue/shared': 3.5.34 + '@vue/reactivity': 3.5.35 + '@vue/runtime-core': 3.5.35 + '@vue/shared': 3.5.35 csstype: 3.2.3 - '@vue/server-renderer@3.5.34(vue@3.5.34(typescript@6.0.3))': + '@vue/server-renderer@3.5.35(vue@3.5.35(typescript@6.0.3))': dependencies: - '@vue/compiler-ssr': 3.5.34 - '@vue/shared': 3.5.34 - vue: 3.5.34(typescript@6.0.3) + '@vue/compiler-ssr': 3.5.35 + '@vue/shared': 3.5.35 + vue: 3.5.35(typescript@6.0.3) - '@vue/shared@3.5.34': {} + '@vue/shared@3.5.35': {} + + abab@2.0.6: {} acorn-globals@7.0.1: dependencies: @@ -5983,16 +6538,22 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 + fast-uri: 3.1.2 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 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 @@ -6014,11 +6575,67 @@ snapshots: aria-query@5.3.2: {} + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + asciinema-player@3.15.1: dependencies: - '@babel/runtime': 7.29.2 - solid-js: 1.9.12 - solid-transition-group: 0.2.3(solid-js@1.9.12) + '@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: {} @@ -6026,9 +6643,19 @@ snapshots: astral-regex@2.0.0: {} + async-function@1.0.0: {} + asynckit@0.4.0: {} - axe-core@4.11.4: {} + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + aws-sign2@0.7.0: {} + + aws4@1.13.2: {} + + axe-core@4.12.0: {} axobject-query@4.1.0: {} @@ -6038,18 +6665,36 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.27: {} + baseline-browser-mapping@2.10.33: {} + + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 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.14: + brace-expansion@1.1.15: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.5: + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -6059,10 +6704,10 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.27 - caniuse-lite: 1.0.30001791 - electron-to-chromium: 1.5.349 - node-releases: 2.0.38 + baseline-browser-mapping: 2.10.33 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.364 + node-releases: 2.0.46 update-browserslist-db: 1.2.3(browserslist@4.28.2) buffer@5.7.1: @@ -6072,26 +6717,53 @@ snapshots: builtin-modules@3.3.0: {} - builtin-modules@5.1.0: {} + builtin-modules@5.2.0: {} bytes@3.1.2: {} - cacheable@2.3.4: + cacheable@2.3.5: dependencies: - '@cacheable/memory': 2.0.8 + '@cacheable/memory': 2.0.9 '@cacheable/utils': 2.4.1 hookified: 1.15.1 keyv: 5.6.0 - qified: 0.9.1 + qified: 0.10.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 callsites@3.1.0: {} camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001791: {} + caniuse-lite@1.0.30001793: {} + + caseless@0.12.0: {} 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 @@ -6109,10 +6781,10 @@ snapshots: dependencies: '@kurkle/color': 0.3.4 - chartjs-adapter-dayjs-4@1.0.4(chart.js@4.5.1)(dayjs@1.11.20): + chartjs-adapter-dayjs-4@1.0.4(chart.js@4.5.1)(dayjs@1.11.21): dependencies: chart.js: 4.5.1 - dayjs: 1.11.20 + dayjs: 1.11.21 chartjs-plugin-zoom@2.2.0(chart.js@4.5.1): dependencies: @@ -6144,8 +6816,16 @@ snapshots: 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: {} + codemirror-lang-elixir@4.0.1: dependencies: '@codemirror/language': 6.12.3 @@ -6153,7 +6833,7 @@ snapshots: codemirror-spell-checker@1.1.2: dependencies: - typo-js: 1.3.1 + typo-js: 1.3.2 codemirror@5.65.21: {} @@ -6173,28 +6853,32 @@ snapshots: commander@14.0.3: {} + commander@2.20.3: {} + commander@4.1.1: {} commander@7.2.0: {} commander@8.3.0: {} - comment-parser@1.4.6: {} + comment-parser@1.4.7: {} compare-versions@6.1.1: {} concat-map@0.0.1: {} - confbox@0.1.8: {} - convert-source-map@2.0.0: {} core-js-compat@3.49.0: 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 @@ -6207,7 +6891,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.2.0 parse-json: 5.2.0 optionalDependencies: typescript: 6.0.3 @@ -6260,17 +6944,17 @@ snapshots: csstype@3.2.3: {} - cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.3): + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.4): dependencies: cose-base: 1.0.3 - cytoscape: 3.33.3 + cytoscape: 3.33.4 - cytoscape-fcose@2.2.0(cytoscape@3.33.3): + cytoscape-fcose@2.2.0(cytoscape@3.33.4): dependencies: cose-base: 2.2.0 - cytoscape: 3.33.3 + cytoscape: 3.33.4 - cytoscape@3.33.3: {} + cytoscape@3.33.4: {} d3-array@2.12.1: dependencies: @@ -6446,13 +7130,35 @@ snapshots: damerau-levenshtein@1.0.8: {} + dashdash@1.14.1: + dependencies: + assert-plus: 1.0.0 + data-urls@3.0.2: dependencies: - abab: '@nolyfill/abab@1.0.44' + abab: 2.0.6 whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 - dayjs@1.11.20: {} + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + dayjs@1.11.21: {} debug@3.2.7: dependencies: @@ -6477,6 +7183,18 @@ snapshots: 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 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + delaunator@5.1.0: dependencies: robust-predicates: 3.0.3 @@ -6517,7 +7235,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.4.2: + dompurify@3.4.7: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -6527,6 +7245,19 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + 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 @@ -6535,7 +7266,14 @@ snapshots: codemirror-spell-checker: 1.1.2 marked: 4.3.0 - electron-to-chromium@1.5.349: {} + 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: {} elkjs@0.9.3: {} @@ -6555,11 +7293,91 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.21 + + es-define-property@1.0.1: {} + es-errors@1.3.0: {} es-module-lexer@2.1.0: {} - es-toolkit@1.46.1: {} + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.4 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + es-toolkit@1.47.0: {} esbuild@0.28.0: optionalDependencies: @@ -6606,234 +7424,234 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)): + eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.7.0)): dependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) - eslint-import-context@0.1.9(unrs-resolver@1.11.1): + eslint-import-context@0.1.9(unrs-resolver@1.12.2): dependencies: get-tsconfig: 4.14.0 stable-hash-x: 0.2.0 optionalDependencies: - unrs-resolver: 1.11.1 + unrs-resolver: 1.12.2 eslint-import-resolver-node@0.3.10: dependencies: debug: 3.2.7 - is-core-module: '@nolyfill/is-core-module@1.0.39' - resolve: 2.0.0-next.6 + is-core-module: 2.16.2 + resolve: 2.0.0-next.7 transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.0(jiti@2.7.0)): + eslint-import-resolver-typescript@4.4.5(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(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)): dependencies: debug: 4.4.3 - eslint: 10.4.0(jiti@2.7.0) - eslint-import-context: 0.1.9(unrs-resolver@1.11.1) + eslint: 10.4.1(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 stable-hash-x: 0.2.0 - tinyglobby: 0.2.16 - unrs-resolver: 1.11.1 + tinyglobby: 0.2.17 + unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(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.60.1(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)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)): + eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.1(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.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.4.0(jiti@2.7.0) + '@typescript-eslint/parser': 8.60.1(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.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.0(jiti@2.7.0)) + eslint-import-resolver-typescript: 4.4.5(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(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-plugin-array-func@5.1.1(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-array-func@5.1.1(eslint@10.4.1(jiti@2.7.0)): dependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) - eslint-plugin-de-morgan@2.1.2(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-de-morgan@2.1.2(eslint@10.4.1(jiti@2.7.0)): dependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) - eslint-plugin-escompat@3.11.4(eslint@10.4.0(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.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) - eslint-plugin-eslint-comments@3.2.0(eslint@10.4.0(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.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) ignore: 5.3.2 - eslint-plugin-filenames@1.3.2(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-filenames@1.3.2(eslint@10.4.1(jiti@2.7.0)): dependencies: - eslint: 10.4.0(jiti@2.7.0) + 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.4)(eslint@10.4.0(jiti@2.7.0)): + 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.0(jiti@2.7.0)) + '@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.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/parser': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) aria-query: 5.3.2 - eslint: 10.4.0(jiti@2.7.0) - eslint-config-prettier: 10.1.8(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-escompat: 3.11.4(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-eslint-comments: 3.2.0(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-filenames: 1.3.2(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-i18n-text: 1.0.1(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@10.4.0(jiti@2.7.0)) + 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.60.1(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-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.5(eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)))(eslint@10.4.0(jiti@2.7.0))(prettier@3.8.3) + 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.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + typescript-eslint: 8.60.1(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.0(jiti@2.7.0)): + eslint-plugin-i18n-text@1.0.1(eslint@10.4.1(jiti@2.7.0)): dependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) - eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(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.59.4 - comment-parser: 1.4.6 + '@typescript-eslint/types': 8.60.1 + comment-parser: 1.4.7 debug: 4.4.3 - eslint: 10.4.0(jiti@2.7.0) - eslint-import-context: 0.1.9(unrs-resolver@1.11.1) + eslint: 10.4.1(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.0 + semver: 7.8.1 stable-hash-x: 0.2.0 - unrs-resolver: 1.11.1 + unrs-resolver: 1.12.2 optionalDependencies: - '@typescript-eslint/utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(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.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(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: '@nolyfill/array-includes@1.0.44' - array.prototype.findlastindex: '@nolyfill/array.prototype.findlastindex@1.0.44' - array.prototype.flat: '@nolyfill/array.prototype.flat@1.0.44' - array.prototype.flatmap: '@nolyfill/array.prototype.flatmap@1.0.44' + 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.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)) - hasown: '@nolyfill/hasown@1.0.44' - is-core-module: '@nolyfill/is-core-module@1.0.39' + eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.60.1(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: '@nolyfill/object.fromentries@1.0.44' - object.groupby: '@nolyfill/object.groupby@1.0.44' - object.values: '@nolyfill/object.values@1.0.44' + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 semver: 6.3.1 - string.prototype.trimend: '@nolyfill/string.prototype.trimend@1.0.44' + string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-jsx-a11y@6.10.2(eslint@10.4.1(jiti@2.7.0)): dependencies: aria-query: 5.3.2 - array-includes: '@nolyfill/array-includes@1.0.44' - array.prototype.flatmap: '@nolyfill/array.prototype.flatmap@1.0.44' + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 - axe-core: 4.11.4 + axe-core: 4.12.0 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 10.4.0(jiti@2.7.0) - hasown: '@nolyfill/hasown@1.0.44' + 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: '@nolyfill/object.fromentries@1.0.44' - safe-regex-test: '@nolyfill/safe-regex-test@1.0.44' - string.prototype.includes: '@nolyfill/string.prototype.includes@1.0.44' + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 eslint-plugin-no-only-tests@3.4.0: {} - eslint-plugin-playwright@2.10.4(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-playwright@2.10.4(eslint@10.4.1(jiti@2.7.0)): dependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) globals: 17.6.0 - eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)))(eslint@10.4.0(jiti@2.7.0))(prettier@3.8.3): + 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.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) prettier: 3.8.3 prettier-linter-helpers: 1.0.1 - synckit: 0.11.12 + synckit: 0.11.13 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@10.4.0(jiti@2.7.0)) + eslint-config-prettier: 10.1.8(eslint@10.4.1(jiti@2.7.0)) - eslint-plugin-regexp@3.1.0(eslint@10.4.0(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.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 - comment-parser: 1.4.6 - eslint: 10.4.0(jiti@2.7.0) + comment-parser: 1.4.7 + eslint: 10.4.1(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.0(jiti@2.7.0)): + eslint-plugin-sonarjs@4.0.3(eslint@10.4.1(jiti@2.7.0)): dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) functional-red-black-tree: 1.0.1 globals: 17.6.0 jsx-ast-utils-x: 0.1.0 lodash.merge: 4.6.2 minimatch: 10.2.5 scslre: 0.3.0 - semver: 7.8.0 + semver: 7.8.1 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 - eslint-plugin-unicorn@64.0.0(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-unicorn@64.0.0(eslint@10.4.1(jiti@2.7.0)): dependencies: - '@babel/helper-validator-identifier': 7.28.5 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) + '@babel/helper-validator-identifier': 7.29.7 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) change-case: 5.4.4 ci-info: 4.4.0 clean-regexp: 1.0.0 core-js-compat: 3.49.0 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) find-up-simple: 1.0.1 globals: 17.6.0 indent-string: 5.0.0 @@ -6842,36 +7660,36 @@ snapshots: pluralize: 8.0.0 regexp-tree: 0.1.27 regjsparser: 0.13.1 - semver: 7.8.0 + semver: 7.8.1 strip-indent: 4.1.1 - eslint-plugin-vue-scoped-css@3.1.0(eslint@10.4.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0))): + 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))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) - es-toolkit: 1.46.1 - eslint: 10.4.0(jiti@2.7.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(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.0(jiti@2.7.0)) + vue-eslint-parser: 10.4.0(eslint@10.4.1(jiti@2.7.0)) - eslint-plugin-vue@10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.4.0(jiti@2.7.0)))(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.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.60.1(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))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) - eslint: 10.4.0(jiti@2.7.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + eslint: 10.4.1(jiti@2.7.0) natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 7.1.1 - semver: 7.8.0 - vue-eslint-parser: 10.4.0(eslint@10.4.0(jiti@2.7.0)) + semver: 7.8.1 + vue-eslint-parser: 10.4.0(eslint@10.4.1(jiti@2.7.0)) xml-name-validator: 4.0.0 optionalDependencies: - '@stylistic/eslint-plugin': 5.10.0(eslint@10.4.0(jiti@2.7.0)) - '@typescript-eslint/parser': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@stylistic/eslint-plugin': 5.10.0(eslint@10.4.1(jiti@2.7.0)) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) - eslint-plugin-wc@3.1.0(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-wc@3.1.0(eslint@10.4.1(jiti@2.7.0)): dependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) is-valid-element-name: 1.0.0 js-levenshtein-esm: 2.0.0 @@ -6880,7 +7698,7 @@ snapshots: eslint-scope@9.1.2: dependencies: '@types/esrecurse': 4.3.1 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 esrecurse: 4.3.0 estraverse: 5.3.0 @@ -6890,18 +7708,18 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.4.0(jiti@2.7.0): + eslint@10.4.1(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.6.0 '@eslint/core': 1.2.1 - '@eslint/plugin-kit': 0.7.1 + '@eslint/plugin-kit': 0.7.2 '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 ajv: 6.15.0 cross-spawn: 7.0.6 debug: 4.4.3 @@ -6955,7 +7773,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 esutils@2.0.3: {} @@ -6963,8 +7781,14 @@ snapshots: 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: {} @@ -6981,7 +7805,7 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-uri@3.1.0: {} + fast-uri@3.1.2: {} fastest-levenshtein@1.0.16: {} @@ -7001,6 +7825,11 @@ 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 @@ -7027,34 +7856,106 @@ snapshots: flat-cache@6.1.22: dependencies: - cacheable: 2.3.4 + cacheable: 2.3.5 flatted: 3.4.2 hookified: 1.15.1 flatted@3.4.2: {} + 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 + form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 - es-set-tostringtag: '@nolyfill/es-set-tostringtag@1.0.44' - hasown: '@nolyfill/hasown@1.0.44' + es-set-tostringtag: 2.1.0 + 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 fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.4 + is-callable: 1.2.7 + functional-red-black-tree@1.0.1: {} - get-east-asian-width@1.5.0: {} + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + get-east-asian-width@1.6.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 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 @@ -7063,6 +7964,15 @@ 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 @@ -7079,6 +7989,11 @@ snapshots: globals@17.6.0: {} + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + globby@16.2.0: dependencies: '@sindresorhus/merge-streams': 4.0.0 @@ -7090,6 +8005,8 @@ snapshots: globjoin@0.1.4: {} + gopd@1.2.0: {} + graceful-fs@4.2.11: {} hachure-fill@0.5.2: {} @@ -7103,19 +8020,50 @@ snapshots: '@types/ws': 8.18.1 entities: 7.0.1 whatwg-mimetype: 3.0.0 - ws: 8.20.0 + ws: 8.21.0 transitivePeerDependencies: - 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-flag@4.0.0: {} has-flag@5.0.1: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hashery@1.5.1: dependencies: hookified: 1.15.1 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + hookified@1.15.1: {} hookified@2.2.0: {} @@ -7141,6 +8089,12 @@ 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 @@ -7150,7 +8104,7 @@ snapshots: iconv-lite@0.6.3: dependencies: - safer-buffer: '@nolyfill/safer-buffer@1.0.44' + safer-buffer: 2.1.2 idiomorph@0.7.4: {} @@ -7171,10 +8125,44 @@ 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 + internmap@1.0.1: {} internmap@2.0.3: {} @@ -7186,46 +8174,156 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-buffer@1.1.6: {} is-builtin-module@5.0.0: dependencies: - builtin-modules: 5.1.0 + builtin-modules: 5.2.0 is-bun-module@2.0.0: dependencies: - semver: 7.8.0 + semver: 7.8.1 + + is-callable@1.2.7: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 is-decimal@2.0.1: {} is-extglob@2.1.1: {} + 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 + is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 is-hexadecimal@2.0.1: {} + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-number@7.0.0: {} is-path-inside@4.0.0: {} is-potential-custom-element-name@1.0.1: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.21 + + is-typedarray@1.0.0: {} + is-valid-element-name@1.0.0: dependencies: is-potential-custom-element-name: 1.0.1 + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + isexe@2.0.0: {} + isstream@0.1.2: {} + jest-environment-jsdom@29.7.0: dependencies: '@jest/environment': 29.7.0 @@ -7243,7 +8341,7 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 @@ -7284,11 +8382,17 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.2.0: + dependencies: + argparse: 2.0.1 + + jsbn@0.1.1: {} + jsdoc-type-pratt-parser@7.2.0: {} jsdom@20.0.3: dependencies: - abab: '@nolyfill/abab@1.0.44' + abab: 2.0.6 acorn: 8.16.0 acorn-globals: 7.0.1 cssom: 0.5.0 @@ -7312,7 +8416,7 @@ snapshots: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 - ws: 8.20.0 + ws: 8.21.0 xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil @@ -7329,29 +8433,52 @@ 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 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: '@nolyfill/array-includes@1.0.44' - array.prototype.flat: '@nolyfill/array.prototype.flat@1.0.44' - object.assign: '@nolyfill/object.assign@1.0.44' - object.values: '@nolyfill/object.values@1.0.44' + 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 + katex@0.17.0: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -7368,6 +8495,10 @@ snapshots: 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: @@ -7441,7 +8572,7 @@ snapshots: lines-and-columns@1.2.4: {} - linkify-it@5.0.0: + linkify-it@5.0.1: dependencies: uc.micro: 2.1.0 @@ -7463,6 +8594,10 @@ snapshots: 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 @@ -7471,7 +8606,7 @@ snapshots: dependencies: argparse: 2.0.1 entities: 4.5.0 - linkify-it: 5.0.0 + linkify-it: 5.0.1 mdurl: 2.0.0 punycode.js: 2.3.1 uc.micro: 2.1.0 @@ -7489,7 +8624,7 @@ snapshots: minimatch: 10.2.5 run-con: 1.3.2 smol-toml: 1.6.1 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 transitivePeerDependencies: - supports-color @@ -7511,13 +8646,16 @@ snapshots: marked@4.3.0: {} - material-icon-theme@5.34.0: + material-icon-theme@5.35.0: 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: {} + mathml-tag-names@4.0.0: {} mdn-data@2.0.28: {} @@ -7533,26 +8671,26 @@ snapshots: mermaid@11.15.0: dependencies: '@braintree/sanitize-url': 7.1.2 - '@iconify/utils': 3.1.1 + '@iconify/utils': 3.1.3 '@mermaid-js/parser': 1.1.1 '@types/d3': 7.4.3 '@upsetjs/venn.js': 2.0.0 - cytoscape: 3.33.3 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.3) - cytoscape-fcose: 2.2.0(cytoscape@3.33.3) + cytoscape: 3.33.4 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.4) + cytoscape-fcose: 2.2.0(cytoscape@3.33.4) d3: 7.9.0 d3-sankey: 0.12.3 dagre-d3-es: 7.0.14 - dayjs: 1.11.20 - dompurify: 3.4.2 - es-toolkit: 1.46.1 + dayjs: 1.11.21 + dompurify: 3.4.7 + es-toolkit: 1.47.0 katex: 0.16.47 khroma: 2.1.0 marked: 16.4.2 roughjs: 4.6.6 stylis: 4.4.0 ts-dedent: 2.2.0 - uuid: 11.1.1 + uuid: 14.0.0 micromark-core-commonmark@2.0.3: dependencies: @@ -7739,27 +8877,22 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 5.0.6 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.14 + brace-expansion: 1.1.15 minimist@1.2.8: {} - mlly@1.8.2: - dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.4 - moo@0.5.3: {} ms@2.1.3: {} muggle-string@0.4.1: {} + mute-stream@0.0.5: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -7776,9 +8909,9 @@ snapshots: node-exports-info@1.6.0: dependencies: - array.prototype.flatmap: '@nolyfill/array.prototype.flatmap@1.0.44' + array.prototype.flatmap: 1.3.3 es-errors: 1.3.0 - object.entries: '@nolyfill/object.entries@1.0.44' + object.entries: 1.1.9 semver: 6.3.1 node-fetch@2.6.13: @@ -7789,9 +8922,7 @@ snapshots: dependencies: whatwg-url: 5.0.0 - node-releases@2.0.38: {} - - nolyfill@1.0.44: {} + node-releases@2.0.46: {} normalize-path@3.0.0: {} @@ -7799,14 +8930,64 @@ snapshots: 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-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + 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 @@ -7822,6 +9003,14 @@ 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 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -7848,7 +9037,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -7863,6 +9052,8 @@ snapshots: path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} path-parse@1.0.7: {} @@ -7873,6 +9064,8 @@ snapshots: perfect-debounce@2.1.0: {} + performance-now@2.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -7883,12 +9076,6 @@ snapshots: pirates@4.0.7: {} - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.2 - pathe: 2.0.3 - playwright-core@1.60.0: {} playwright@1.60.0: @@ -7906,6 +9093,8 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 + possible-typed-array-names@1.1.0: {} + postcss-html@1.8.1: dependencies: htmlparser2: 8.0.2 @@ -7985,10 +9174,12 @@ snapshots: punycode@2.3.1: {} - qified@0.9.1: + qified@0.10.1: dependencies: hookified: 2.2.0 + qs@6.5.5: {} + querystringify@2.2.0: {} queue-microtask@1.2.3: {} @@ -8003,10 +9194,29 @@ 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 + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regenerator-runtime@0.9.6: {} + regexp-ast-analysis@0.7.1: dependencies: '@eslint-community/regexpp': 4.12.2 @@ -8014,12 +9224,50 @@ snapshots: regexp-tree@0.1.27: {} + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + regjsparser@0.13.1: 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: {} @@ -8031,47 +9279,56 @@ snapshots: resolve@1.22.12: dependencies: es-errors: 1.3.0 - is-core-module: '@nolyfill/is-core-module@1.0.39' + is-core-module: 2.16.2 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@2.0.0-next.6: + resolve@2.0.0-next.7: dependencies: es-errors: 1.3.0 - is-core-module: '@nolyfill/is-core-module@1.0.39' + is-core-module: 2.16.2 node-exports-info: 1.6.0 - object-keys: '@nolyfill/object-keys@1.0.44' + 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 + reusify@1.1.0: {} + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + robust-predicates@3.0.3: {} - rolldown-license-plugin@3.0.7(rolldown@1.0.1): + rolldown-license-plugin@3.0.9(rolldown@1.0.3): dependencies: - rolldown: 1.0.1 + rolldown: 1.0.3 - rolldown@1.0.1: + rolldown@1.0.3: dependencies: - '@oxc-project/types': 0.130.0 + '@oxc-project/types': 0.133.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.1 - '@rolldown/binding-darwin-arm64': 1.0.1 - '@rolldown/binding-darwin-x64': 1.0.1 - '@rolldown/binding-freebsd-x64': 1.0.1 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.1 - '@rolldown/binding-linux-arm64-gnu': 1.0.1 - '@rolldown/binding-linux-arm64-musl': 1.0.1 - '@rolldown/binding-linux-ppc64-gnu': 1.0.1 - '@rolldown/binding-linux-s390x-gnu': 1.0.1 - '@rolldown/binding-linux-x64-gnu': 1.0.1 - '@rolldown/binding-linux-x64-musl': 1.0.1 - '@rolldown/binding-openharmony-arm64': 1.0.1 - '@rolldown/binding-wasm32-wasi': 1.0.1 - '@rolldown/binding-win32-arm64-msvc': 1.0.1 - '@rolldown/binding-win32-x64-msvc': 1.0.1 + '@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 roughjs@4.6.6: dependencies: @@ -8080,6 +9337,10 @@ 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 @@ -8093,6 +9354,31 @@ snapshots: rw@1.3.3: {} + rx-lite@3.1.2: {} + + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + sax@1.6.0: {} saxes@6.0.0: @@ -8107,13 +9393,35 @@ snapshots: semver@6.3.1: {} - semver@7.8.0: {} + semver@7.8.1: {} - seroval-plugins@1.5.3(seroval@1.5.3): + seroval-plugins@1.5.4(seroval@1.5.4): dependencies: - seroval: 1.5.3 + seroval: 1.5.4 - seroval@1.5.3: {} + seroval@1.5.4: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 shebang-command@2.0.0: dependencies: @@ -8121,6 +9429,34 @@ snapshots: shebang-regex@3.0.0: {} + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -8137,17 +9473,17 @@ snapshots: smol-toml@1.6.1: {} - solid-js@1.9.12: + solid-js@1.9.13: dependencies: csstype: 3.2.3 - seroval: 1.5.3 - seroval-plugins: 1.5.3(seroval@1.5.3) + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) - solid-transition-group@0.2.3(solid-js@1.9.12): + solid-transition-group@0.2.3(solid-js@1.9.13): dependencies: - '@solid-primitives/refs': 1.1.3(solid-js@1.9.12) - '@solid-primitives/transition-group': 1.1.2(solid-js@1.9.12) - solid-js: 1.9.12 + '@solid-primitives/refs': 1.1.3(solid-js@1.9.13) + '@solid-primitives/transition-group': 1.1.2(solid-js@1.9.13) + solid-js: 1.9.13 sortablejs@1.15.7: {} @@ -8160,6 +9496,18 @@ snapshots: 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: @@ -8170,6 +9518,17 @@ snapshots: std-env@4.1.0: {} + stop-iteration-iterator@1.1.0: + 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 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -8178,14 +9537,47 @@ snapshots: string-width@8.1.0: dependencies: - get-east-asian-width: 1.5.0 + get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 string-width@8.2.1: dependencies: - get-east-asian-width: 1.5.0 + 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 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + 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 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -8224,9 +9616,9 @@ snapshots: stylelint@17.12.0(typescript@6.0.3): dependencies: - '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-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.3(css-tree@3.2.1) + '@csstools/css-syntax-patches-for-csstree': 1.1.4(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) @@ -8272,11 +9664,13 @@ snapshots: lines-and-columns: 1.2.4 mz: 2.7.0 pirates: 4.0.7 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 ts-interface-checker: 0.1.13 supports-color@10.2.2: {} + supports-color@2.0.0: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -8320,9 +9714,9 @@ snapshots: transitivePeerDependencies: - encoding - synckit@0.11.12: + synckit@0.11.13: dependencies: - '@pkgr/core': 0.2.9 + '@pkgr/core': 0.3.6 table@6.9.0: dependencies: @@ -8372,11 +9766,13 @@ snapshots: throttle-debounce@5.0.2: {} + through@2.3.8: {} + tinybench@2.9.0: {} - tinyexec@1.1.2: {} + tinyexec@1.2.4: {} - tinyglobby@0.2.16: + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 @@ -8393,6 +9789,11 @@ 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 @@ -8429,30 +9830,69 @@ snapshots: 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: {} - typescript-eslint@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3): + typed-array-buffer@1.0.3: dependencies: - '@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/parser': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - eslint: 10.4.0(jiti@2.7.0) + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.8: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(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.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.1(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.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3): + typescript-eslint@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/parser': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) - '@typescript-eslint/utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.4.0(jiti@2.7.0) + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(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.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.4.1(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -8461,43 +9901,53 @@ snapshots: typescript@6.0.3: {} - typo-js@1.3.1: {} + typo-js@1.3.2: {} uc.micro@2.1.0: {} - ufo@1.6.4: {} - uint8-to-base64@0.2.1: {} + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + undici-types@7.24.6: {} unicorn-magic@0.4.0: {} universalify@0.2.0: {} - unrs-resolver@1.11.1: + unrs-resolver@1.12.2: dependencies: napi-postinstall: 0.3.4 optionalDependencies: - '@unrs/resolver-binding-android-arm-eabi': 1.11.1 - '@unrs/resolver-binding-android-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-x64': 1.11.1 - '@unrs/resolver-binding-freebsd-x64': 1.11.1 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 - '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-musl': 1.11.1 - '@unrs/resolver-binding-wasm32-wasi': 1.11.1 - '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 - '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 - '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@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: @@ -8505,7 +9955,7 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - updates@17.16.13: {} + updates@17.17.3: {} uri-js@4.4.1: dependencies: @@ -8516,38 +9966,50 @@ 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@11.1.1: {} + uuid@14.0.0: {} + + uuid@3.4.0: {} vanilla-colorful@0.7.2: {} - vite-string-plugin@2.0.4(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)): + verror@1.10.0: dependencies: - vite: 8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 - vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0): + vite-string-plugin@2.0.4(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)): + dependencies: + vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + + vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.15 - rolldown: 1.0.1 - tinyglobby: 0.2.16 + rolldown: 1.0.3 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 25.9.1 esbuild: 0.28.0 fsevents: 2.3.3 jiti: 2.7.0 - vitest@4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)): + vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)): dependencies: - '@vitest/expect': 4.1.7 - '@vitest/mocker': 4.1.7(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) - '@vitest/pretty-format': 4.1.7 - '@vitest/runner': 4.1.7 - '@vitest/snapshot': 4.1.7 - '@vitest/spy': 4.1.7 - '@vitest/utils': 4.1.7 + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(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 es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -8556,10 +10018,10 @@ snapshots: picomatch: 4.0.4 std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.1.2 - tinyglobby: 0.2.16 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.9.1 @@ -8572,40 +10034,40 @@ snapshots: vue-bar-graph@2.2.0(typescript@6.0.3): dependencies: - vue: 3.5.34(typescript@6.0.3) + vue: 3.5.35(typescript@6.0.3) transitivePeerDependencies: - typescript - vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.34(typescript@6.0.3)): + vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.35(typescript@6.0.3)): dependencies: chart.js: 4.5.1 - vue: 3.5.34(typescript@6.0.3) + vue: 3.5.35(typescript@6.0.3) - vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0)): + vue-eslint-parser@10.4.0(eslint@10.4.1(jiti@2.7.0)): dependencies: debug: 4.4.3 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(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.0 + semver: 7.8.1 transitivePeerDependencies: - supports-color - vue-tsc@3.3.1(typescript@6.0.3): + vue-tsc@3.3.3(typescript@6.0.3): dependencies: '@volar/typescript': 2.4.28 - '@vue/language-core': 3.3.1 + '@vue/language-core': 3.3.3 typescript: 6.0.3 - vue@3.5.34(typescript@6.0.3): + vue@3.5.35(typescript@6.0.3): dependencies: - '@vue/compiler-dom': 3.5.34 - '@vue/compiler-sfc': 3.5.34 - '@vue/runtime-dom': 3.5.34 - '@vue/server-renderer': 3.5.34(vue@3.5.34(typescript@6.0.3)) - '@vue/shared': 3.5.34 + '@vue/compiler-dom': 3.5.35 + '@vue/compiler-sfc': 3.5.35 + '@vue/runtime-dom': 3.5.35 + '@vue/server-renderer': 3.5.35(vue@3.5.35(typescript@6.0.3)) + '@vue/shared': 3.5.35 optionalDependencies: typescript: 6.0.3 @@ -8635,6 +10097,47 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.21 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.21: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@1.3.1: dependencies: isexe: 2.0.0 @@ -8650,11 +10153,13 @@ snapshots: word-wrap@1.2.5: {} + wrappy@1.0.2: {} + write-file-atomic@7.0.1: dependencies: signal-exit: 4.1.0 - ws@8.20.0: {} + ws@8.21.0: {} xml-lexer@0.2.2: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0c8f7c63fef..c16cfb76cba 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,3 @@ -packages: [ . ] # workaround for https://github.com/SukkaW/nolyfill/issues/119 savePrefix: '' dedupePeerDependents: false updateNotifier: false @@ -8,28 +7,6 @@ peerDependencyRules: allowedVersions: eslint-plugin-github>eslint: '>=9' -overrides: - array-includes: npm:@nolyfill/array-includes@^1 - array.prototype.findlastindex: npm:@nolyfill/array.prototype.findlastindex@^1 - array.prototype.flat: npm:@nolyfill/array.prototype.flat@^1 - array.prototype.flatmap: npm:@nolyfill/array.prototype.flatmap@^1 - es-aggregate-error: npm:@nolyfill/es-aggregate-error@^1 - hasown: npm:@nolyfill/hasown@^1 - is-core-module: npm:@nolyfill/is-core-module@^1 - object.assign: npm:@nolyfill/object.assign@^1 - object.fromentries: npm:@nolyfill/object.fromentries@^1 - object.groupby: npm:@nolyfill/object.groupby@^1 - object.values: npm:@nolyfill/object.values@^1 - safe-buffer: npm:@nolyfill/safe-buffer@^1 - safe-regex-test: npm:@nolyfill/safe-regex-test@^1 - safer-buffer: npm:@nolyfill/safer-buffer@^1 - string.prototype.includes: npm:@nolyfill/string.prototype.includes@^1 - string.prototype.trimend: npm:@nolyfill/string.prototype.trimend@^1 - object-keys: npm:@nolyfill/object-keys@^1 - object.entries: npm:@nolyfill/object.entries@^1 - abab: npm:@nolyfill/abab@^1 - es-set-tostringtag: npm:@nolyfill/es-set-tostringtag@^1 - allowBuilds: '@scarf/scarf': false core-js: false diff --git a/public/assets/img/svg/octicon-flag.svg b/public/assets/img/svg/octicon-flag.svg new file mode 100644 index 00000000000..1d757dca08d --- /dev/null +++ b/public/assets/img/svg/octicon-flag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/img/svg/octicon-vscode.svg b/public/assets/img/svg/octicon-vscode.svg index 04ac8cacd51..81e0f7cbb07 100644 --- a/public/assets/img/svg/octicon-vscode.svg +++ b/public/assets/img/svg/octicon-vscode.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/renovate.json5 b/renovate.json5 index 6f86c61e515..bbfbb8d78a0 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -95,7 +95,7 @@ "matchManagers": ["npm"], "postUpdateOptions": ["pnpmDedupe"], "postUpgradeTasks": { - "commands": ["make svg nolyfill"], + "commands": ["make svg"], "fileFilters": ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", "public/assets/img/svg/**"], "executionMode": "branch", }, diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index d82ac6988c8..eeea05c9b44 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -121,6 +121,9 @@ func ArtifactsRoutes(prefix string) *web.Router { m.Get("/{artifact_id}/download", r.downloadArtifact) }) + // Job summary upload endpoint (GITHUB_STEP_SUMMARY). + m.Put(jobSummaryRouteBase, uploadJobSummary) + return m } diff --git a/routers/api/actions/job_summary.go b/routers/api/actions/job_summary.go new file mode 100644 index 00000000000..fa21cd6d11c --- /dev/null +++ b/routers/api/actions/job_summary.go @@ -0,0 +1,104 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "errors" + "io" + "mime" + "net/http" + "slices" + "strconv" + + actions_model "gitea.dev/models/actions" + "gitea.dev/modules/log" + "gitea.dev/modules/util" +) + +const jobSummaryRouteBase = "/_apis/pipelines/workflows/{run_id}/jobs/{job_id}/steps/{step_index}/summary" + +func uploadJobSummary(ctx *ArtifactContext) { + task, _, ok := validateRunID(ctx) + if !ok { + return + } + + jobID := ctx.PathParamInt64("job_id") + if jobID <= 0 || task.Job.ID != jobID { + ctx.HTTPError(http.StatusBadRequest, "job_id mismatch") + return + } + + stepIndex, err := strconv.ParseInt(ctx.PathParam("step_index"), 10, 64) + if err != nil || stepIndex < 0 { + ctx.HTTPError(http.StatusBadRequest, "invalid step_index") + return + } + steps, err := actions_model.GetTaskStepsByTaskID(ctx, task.ID) + if err != nil { + log.Error("Error getting task steps: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error getting task steps") + return + } + if !slices.ContainsFunc(steps, func(s *actions_model.ActionTaskStep) bool { return s.Index == stepIndex }) { + ctx.HTTPError(http.StatusBadRequest, "step_index mismatch") + return + } + + contentType, ok := normalizeJobSummaryContentType(ctx.Req.Header.Get("Content-Type")) + if !ok { + ctx.HTTPError(http.StatusBadRequest, "invalid summary content type") + return + } + + body, err := io.ReadAll(io.LimitReader(ctx.Req.Body, actions_model.MaxJobSummarySize+1)) + if err != nil { + log.Error("Error reading job summary request body: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "read request body") + return + } + message := "success" + if len(body) == 0 { + // PUT with an empty body clears any previously-stored summary for this step. + if err := actions_model.DeleteActionRunJobSummary(ctx, task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, task.Job.ID, stepIndex); err != nil { + log.Error("Error deleting job summary: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error deleting job summary") + return + } + message = "cleared" + } else if err := actions_model.UpsertActionRunJobSummary(ctx, task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, task.Job.ID, stepIndex, contentType, body); err != nil { + if errors.Is(err, actions_model.ErrJobSummaryAggregateExceeded) { + ctx.HTTPError(http.StatusBadRequest, "job summary aggregate size exceeded") + return + } + if errors.Is(err, util.ErrInvalidArgument) { + ctx.HTTPError(http.StatusBadRequest, "invalid summary") + return + } + log.Error("Error upsert job summary: %v", err) + ctx.HTTPError(http.StatusInternalServerError, "Error upsert job summary") + return + } + + ctx.JSON(http.StatusOK, map[string]any{ + "message": message, + "sizeBytes": len(body), + "runAttempt": task.Job.RunAttemptID, + }) +} + +func normalizeJobSummaryContentType(contentType string) (string, bool) { + if contentType == "" || contentType == "application/octet-stream" { + return actions_model.JobSummaryContentTypeMarkdown, true + } + + mediaType, _, err := mime.ParseMediaType(contentType) + if err != nil { + return "", false + } + if mediaType != actions_model.JobSummaryContentTypeMarkdown { + return "", false + } + return actions_model.JobSummaryContentTypeMarkdown, true +} diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go index e98aef95153..803b7c13a12 100644 --- a/routers/api/actions/runner/runner.go +++ b/routers/api/actions/runner/runner.go @@ -161,7 +161,7 @@ func (s *Service) Declare( return nil, status.Errorf(codes.Internal, "update runner: %v", err) } - return connect.NewResponse(&runnerv1.DeclareResponse{ + resp := connect.NewResponse(&runnerv1.DeclareResponse{ Runner: &runnerv1.Runner{ Id: runner.ID, Uuid: runner.UUID, @@ -170,7 +170,11 @@ func (s *Service) Declare( Version: runner.Version, Labels: runner.AgentLabels, }, - }), nil + }) + // Capabilities are communicated via headers to avoid a hard dependency on a proto bump. + // Older runners ignore unknown headers; newer runners can use this for feature negotiation. + resp.Header().Set("X-Gitea-Actions-Capabilities", actions_model.RunnerCapabilities()) + return resp, nil } // FetchTask assigns a task to the runner diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go index 6375748086c..4713c7a1c78 100644 --- a/routers/api/v1/admin/org.go +++ b/routers/api/v1/admin/org.go @@ -67,7 +67,7 @@ func CreateOrg(ctx *context.APIContext) { db.IsErrNameReserved(err) || db.IsErrNameCharsNotAllowed(err) || db.IsErrNamePatternNotAllowed(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index d7f10b75b0b..9441fe0c18e 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -40,7 +40,7 @@ func parseAuthSource(ctx *context.APIContext, u *user_model.User, sourceID int64 source, err := auth.GetSourceByID(ctx, sourceID) if err != nil { if auth.IsErrSourceNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -97,14 +97,12 @@ func CreateUser(ctx *context.APIContext) { if u.LoginType == auth.Plain { if len(form.Password) < setting.MinPasswordLength { - err := errors.New("PasswordIsRequired") - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, "PasswordIsRequired") return } if !password.IsComplexEnough(form.Password) { - err := errors.New("PasswordComplexity") - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, "PasswordComplexity") return } @@ -112,7 +110,7 @@ func CreateUser(ctx *context.APIContext) { if password.IsErrIsPwnedRequest(err) { log.Error(err.Error()) } - ctx.APIError(http.StatusBadRequest, errors.New("PasswordPwned")) + ctx.APIError(http.StatusBadRequest, "PasswordPwned") return } } @@ -143,7 +141,7 @@ func CreateUser(ctx *context.APIContext) { user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) || db.IsErrNamePatternNotAllowed(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -204,11 +202,11 @@ func EditUser(ctx *context.APIContext) { if err := user_service.UpdateAuth(ctx, ctx.ContextUser, authOpts); err != nil { switch { case errors.Is(err, password.ErrMinLength): - ctx.APIError(http.StatusBadRequest, fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength)) + ctx.APIError(http.StatusBadRequest, fmt.Sprintf("password must be at least %d characters", setting.MinPasswordLength)) case errors.Is(err, password.ErrComplexity): - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) case errors.Is(err, password.ErrIsPwned), password.IsErrIsPwnedRequest(err): - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) default: ctx.APIErrorInternal(err) } @@ -222,9 +220,9 @@ func EditUser(ctx *context.APIContext) { if !user_model.IsEmailDomainAllowed(*form.Email) { err = fmt.Errorf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email) } - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) case user_model.IsErrEmailAlreadyUsed(err): - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) default: ctx.APIErrorInternal(err) } @@ -249,7 +247,7 @@ func EditUser(ctx *context.APIContext) { if err := user_service.UpdateUser(ctx, ctx.ContextUser, opts); err != nil { if user_model.IsErrDeleteLastAdminUser(err) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -289,13 +287,13 @@ func DeleteUser(ctx *context.APIContext) { // "$ref": "#/responses/validationError" if ctx.ContextUser.IsOrganization() { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name)) + ctx.APIError(http.StatusUnprocessableEntity, "target is an organization but not user") return } // admin should not delete themself if ctx.ContextUser.ID == ctx.Doer.ID { - ctx.APIError(http.StatusUnprocessableEntity, errors.New("you cannot delete yourself")) + ctx.APIError(http.StatusUnprocessableEntity, "you cannot delete yourself") return } @@ -304,7 +302,7 @@ func DeleteUser(ctx *context.APIContext) { org_model.IsErrUserHasOrgs(err) || packages_model.IsErrUserOwnPackages(err) || user_model.IsErrDeleteLastAdminUser(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -471,13 +469,11 @@ func SearchUsers(ctx *context.APIContext) { var visible []api.VisibleType visibilityParam := ctx.FormString("visibility") - if len(visibilityParam) > 0 { - if visibility, ok := api.VisibilityModes[visibilityParam]; ok { - visible = []api.VisibleType{visibility} - } else { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid visibility: \"%s\"", visibilityParam)) - return - } + if visibility, ok := api.VisibilityModes[visibilityParam]; ok { + visible = []api.VisibleType{visibility} + } else if visibilityParam != "" { + ctx.APIError(http.StatusUnprocessableEntity, "invalid visibility") + return } searchOpts := user_model.SearchUserOptions{ @@ -551,7 +547,7 @@ func RenameUser(ctx *context.APIContext) { // "$ref": "#/responses/validationError" if ctx.ContextUser.IsOrganization() { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name)) + ctx.APIError(http.StatusUnprocessableEntity, "target is an organization but not user") return } @@ -560,7 +556,7 @@ func RenameUser(ctx *context.APIContext) { // Check if username has been changed if err := user_service.RenameUser(ctx, ctx.ContextUser, newName, ctx.Doer); err != nil { if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index eb5f52fd575..618181eb1c1 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -151,7 +151,7 @@ func repoAssignment() func(ctx *context.APIContext) { if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil { context.RedirectToUser(ctx.Base, ctx.Doer, userName, redirectUserID) } else if user_model.IsErrUserRedirectNotExist(err) { - ctx.APIErrorNotFound("GetUserByName", err) + ctx.APIErrorNotFound() } else { ctx.APIErrorInternal(err) } @@ -626,7 +626,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { if err == nil { context.RedirectToUser(ctx.Base, ctx.Doer, ctx.PathParam("org"), redirectUserID) } else if user_model.IsErrUserRedirectNotExist(err) { - ctx.APIErrorNotFound("GetOrgByName", err) + ctx.APIErrorNotFound() } else { ctx.APIErrorInternal(err) } @@ -734,14 +734,14 @@ func mustEnableWiki(ctx *context.APIContext) { // FIXME: for consistency, maybe most mustNotBeArchived checks should be replaced with mustEnableEditor func mustNotBeArchived(ctx *context.APIContext) { if ctx.Repo.Repository.IsArchived { - ctx.APIError(http.StatusLocked, fmt.Errorf("%s is archived", ctx.Repo.Repository.FullName())) + ctx.APIError(http.StatusLocked, "repo is archived") return } } func mustEnableEditor(ctx *context.APIContext) { if !ctx.Repo.Repository.CanEnableEditor() { - ctx.APIError(http.StatusLocked, fmt.Errorf("%s is not allowed to edit", ctx.Repo.Repository.FullName())) + ctx.APIError(http.StatusLocked, "repo is not allowed to edit") return } } @@ -862,12 +862,12 @@ func individualPermsChecker(ctx *context.APIContext) { switch ctx.ContextUser.Visibility { case api.VisibleTypePrivate: if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) { - ctx.APIErrorNotFound("Visit Project", nil) + ctx.APIErrorNotFound() return } case api.VisibleTypeLimited: if ctx.Doer == nil { - ctx.APIErrorNotFound("Visit Project", nil) + ctx.APIErrorNotFound() return } } diff --git a/routers/api/v1/notify/notifications.go b/routers/api/v1/notify/notifications.go index 92aae46a784..8e032292c2e 100644 --- a/routers/api/v1/notify/notifications.go +++ b/routers/api/v1/notify/notifications.go @@ -28,7 +28,7 @@ func NewAvailable(ctx *context.APIContext) { Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread}, }) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } @@ -38,7 +38,7 @@ func NewAvailable(ctx *context.APIContext) { func getFindNotificationOptions(ctx *context.APIContext) *activities_model.FindNotificationOptions { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return nil } opts := &activities_model.FindNotificationOptions{ diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go index 16996907508..7ba4469ff66 100644 --- a/routers/api/v1/notify/repo.go +++ b/routers/api/v1/notify/repo.go @@ -183,7 +183,7 @@ func ReadRepoNotifications(ctx *context.APIContext) { if len(qLastRead) > 0 { tmpLastRead, err := time.Parse(time.RFC3339, qLastRead) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } if !tmpLastRead.IsZero() { diff --git a/routers/api/v1/notify/threads.go b/routers/api/v1/notify/threads.go index 2b8c91169f8..c66db13f8b6 100644 --- a/routers/api/v1/notify/threads.go +++ b/routers/api/v1/notify/threads.go @@ -104,14 +104,14 @@ func getThread(ctx *context.APIContext) *activities_model.Notification { n, err := activities_model.GetNotificationByID(ctx, ctx.PathParamInt64("id")) if err != nil { if db.IsErrNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } return nil } if n.UserID != ctx.Doer.ID && !ctx.Doer.IsAdmin { - ctx.APIError(http.StatusForbidden, fmt.Errorf("only user itself and admin are allowed to read/change this thread %d", n.ID)) + ctx.APIError(http.StatusForbidden, fmt.Sprintf("only user itself and admin are allowed to read/change this thread %d", n.ID)) return nil } return n diff --git a/routers/api/v1/notify/user.go b/routers/api/v1/notify/user.go index 2e54eb178d4..9894f577c54 100644 --- a/routers/api/v1/notify/user.go +++ b/routers/api/v1/notify/user.go @@ -134,7 +134,7 @@ func ReadNotifications(ctx *context.APIContext) { if len(qLastRead) > 0 { tmpLastRead, err := time.Parse(time.RFC3339, qLastRead) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } if !tmpLastRead.IsZero() { diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go index d63a134bd55..3f1135dcf3f 100644 --- a/routers/api/v1/org/action.go +++ b/routers/api/v1/org/action.go @@ -111,9 +111,9 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) { _, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -158,9 +158,9 @@ func (Action) DeleteSecret(ctx *context.APIContext) { err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname")) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -277,7 +277,7 @@ func (Action) GetVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -327,9 +327,9 @@ func (Action) DeleteVariable(ctx *context.APIContext) { if err := actions_service.DeleteVariableByName(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("variablename")); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -387,13 +387,13 @@ func (Action) CreateVariable(ctx *context.APIContext) { return } if v != nil && v.ID > 0 { - ctx.APIError(http.StatusConflict, util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + ctx.APIError(http.StatusConflict, "variable name already exists") return } if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value, opt.Description); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -445,7 +445,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -462,7 +462,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) { if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/org/avatar.go b/routers/api/v1/org/avatar.go index 38b0655bea9..7e6f8a77c91 100644 --- a/routers/api/v1/org/avatar.go +++ b/routers/api/v1/org/avatar.go @@ -39,7 +39,7 @@ func UpdateAvatar(ctx *context.APIContext) { content, err := base64.StdEncoding.DecodeString(form.Image) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go index 4c3365e6a6d..443003d5de2 100644 --- a/routers/api/v1/org/label.go +++ b/routers/api/v1/org/label.go @@ -90,7 +90,7 @@ func CreateLabel(ctx *context.APIContext) { form.Color = strings.Trim(form.Color, " ") color, err := label.NormalizeColor(form.Color) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } form.Color = color @@ -209,7 +209,7 @@ func EditLabel(ctx *context.APIContext) { if form.Color != nil { color, err := label.NormalizeColor(*form.Color) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } l.Color = color diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go index 957544d1047..11b46a05c1c 100644 --- a/routers/api/v1/org/member.go +++ b/routers/api/v1/org/member.go @@ -221,7 +221,7 @@ func checkCanChangeOrgUserStatus(ctx *context.APIContext, targetUser *user_model // allow org owners to change status of members isOwner, err := ctx.Org.Organization.IsOwnedBy(ctx, ctx.Doer.ID) if err != nil { - ctx.APIError(http.StatusInternalServerError, err) + ctx.APIErrorInternal(err) } else if !isOwner { ctx.APIError(http.StatusForbidden, "Cannot change member visibility") } diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index 87e847d6421..8f4d19719fd 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -146,7 +146,7 @@ func GetUserOrgsPermissions(ctx *context.APIContext) { op := api.OrganizationPermissions{} if !organization.HasOrgOrUserVisible(ctx, o, ctx.Doer) { - ctx.APIErrorNotFound("HasOrgOrUserVisible", nil) + ctx.APIErrorNotFound() return } @@ -256,7 +256,7 @@ func Create(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.CreateOrgOption) if !ctx.Doer.CanCreateOrganization() { - ctx.APIError(http.StatusForbidden, nil) + ctx.APIError(http.StatusForbidden, "not allowed to create org") return } @@ -282,7 +282,7 @@ func Create(ctx *context.APIContext) { db.IsErrNameReserved(err) || db.IsErrNameCharsNotAllowed(err) || db.IsErrNamePatternNotAllowed(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -312,7 +312,7 @@ func Get(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) { - ctx.APIErrorNotFound("HasOrgOrUserVisible", nil) + ctx.APIErrorNotFound() return } @@ -355,7 +355,7 @@ func Rename(ctx *context.APIContext) { orgUser := ctx.Org.Organization.AsUser() if err := user_service.RenameUser(ctx, orgUser, form.NewName, ctx.Doer); err != nil { if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -394,7 +394,7 @@ func Edit(ctx *context.APIContext) { if err := org.UpdateOrgEmailAddress(ctx, ctx.Org.Organization, form.Email); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index d8acc767c58..4373b90f2fa 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -236,7 +236,7 @@ func CreateTeam(ctx *context.APIContext) { if err := org_service.NewTeam(ctx, team); err != nil { if organization.IsErrTeamAlreadyExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -490,7 +490,7 @@ func AddTeamMember(ctx *context.APIContext) { } if err := org_service.AddTeamMember(ctx, ctx.Org.Team, u); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index 6b01023ef91..d06d2a636b8 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -329,7 +329,7 @@ func GetLatestPackageVersion(ctx *context.APIContext) { return } if len(pvs) == 0 { - ctx.APIError(http.StatusNotFound, err) + ctx.APIErrorNotFound() return } @@ -383,7 +383,7 @@ func LinkPackage(ctx *context.APIContext) { pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name")) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -393,7 +393,7 @@ func LinkPackage(ctx *context.APIContext) { repo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, ctx.PathParam("repo_name")) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -404,9 +404,9 @@ func LinkPackage(ctx *context.APIContext) { if err != nil { switch { case errors.Is(err, util.ErrInvalidArgument): - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) case errors.Is(err, util.ErrPermissionDenied): - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) default: ctx.APIErrorInternal(err) } @@ -445,7 +445,7 @@ func UnlinkPackage(ctx *context.APIContext) { pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name")) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -456,9 +456,9 @@ func UnlinkPackage(ctx *context.APIContext) { if err != nil { switch { case errors.Is(err, util.ErrPermissionDenied): - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) case errors.Is(err, util.ErrInvalidArgument): - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) default: ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 169ed4b68ec..60077474120 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -141,9 +141,9 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) { _, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, repo.ID, ctx.PathParam("secretname"), opt.Data, opt.Description) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -195,9 +195,9 @@ func (Action) DeleteSecret(ctx *context.APIContext) { err := secret_service.DeleteSecretByName(ctx, 0, repo.ID, ctx.PathParam("secretname")) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -243,7 +243,7 @@ func (Action) GetVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -298,9 +298,9 @@ func (Action) DeleteVariable(ctx *context.APIContext) { if err := actions_service.DeleteVariableByName(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParam("variablename")); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -361,13 +361,13 @@ func (Action) CreateVariable(ctx *context.APIContext) { return } if v != nil && v.ID > 0 { - ctx.APIError(http.StatusConflict, util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + ctx.APIError(http.StatusConflict, "variable name already exists") return } if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value, opt.Description); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -422,7 +422,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -439,7 +439,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) { if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -957,7 +957,7 @@ func ActionsGetWorkflow(ctx *context.APIContext) { workflow, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -1005,7 +1005,7 @@ func ActionsDisableWorkflow(ctx *context.APIContext) { err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, false) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -1062,7 +1062,7 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) { workflowID := ctx.PathParam("workflow_id") opt := web.GetForm(ctx).(*api.CreateActionWorkflowDispatch) if opt.Ref == "" { - ctx.APIError(http.StatusUnprocessableEntity, util.NewInvalidArgumentErrorf("ref is required parameter")) + ctx.APIError(http.StatusUnprocessableEntity, "ref is required parameter") return } @@ -1088,11 +1088,11 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else if errors.Is(err, util.ErrPermissionDenied) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -1151,7 +1151,7 @@ func ActionsEnableWorkflow(ctx *context.APIContext) { err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, true) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -1164,11 +1164,8 @@ func ActionsEnableWorkflow(ctx *context.APIContext) { func getCurrentRepoActionRunByID(ctx *context.APIContext) *actions_model.ActionRun { runID := ctx.PathParamInt64("run") run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID) - if errors.Is(err, util.ErrNotExist) { - ctx.APIErrorNotFound(err) - return nil - } else if err != nil { - ctx.APIErrorInternal(err) + if err != nil { + ctx.APIErrorAuto(err) return nil } run.Repo = ctx.Repo.Repository @@ -1198,11 +1195,8 @@ func getCurrentRepoActionRunAttemptByNumber(ctx *context.APIContext) (*actions_m attemptNum := ctx.PathParamInt64("attempt") attempt, err := actions_model.GetRunAttemptByRunIDAndAttemptNum(ctx, run.ID, attemptNum) - if errors.Is(err, util.ErrNotExist) { - ctx.APIErrorNotFound(err) - return nil, nil - } else if err != nil { - ctx.APIErrorInternal(err) + if err != nil { + ctx.APIErrorAuto(err) return nil, nil } return run, attempt @@ -1454,7 +1448,7 @@ func RerunWorkflowJob(ctx *context.APIContext) { jobID := ctx.PathParamInt64("job_id") jobIdx := slices.IndexFunc(jobs, func(job *actions_model.ActionRunJob) bool { return job.ID == jobID }) if jobIdx == -1 { - ctx.APIErrorNotFound(util.NewNotExistErrorf("workflow job with id %d", jobID)) + ctx.APIErrorNotFound("workflow job not found") return } @@ -1490,13 +1484,13 @@ func RerunWorkflowJob(ctx *context.APIContext) { func handleWorkflowRerunError(ctx *context.APIContext, err error) { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } else if errors.Is(err, util.ErrAlreadyExist) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) return } ctx.APIErrorInternal(err) @@ -1560,17 +1554,13 @@ func ListWorkflowRunJobs(ctx *context.APIContext) { // Avoid the list all jobs functionality for this api route to be used with a runID == 0. if runID <= 0 { - ctx.APIError(http.StatusBadRequest, util.NewInvalidArgumentErrorf("runID must be a positive integer")) + ctx.APIError(http.StatusBadRequest, "runID must be a positive integer") return } run, err := actions_model.GetRunByRepoAndID(ctx, repoID, runID) if err != nil { - if errors.Is(err, util.ErrNotExist) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } // runID is used as an additional filter next to repoID to ensure that we only list jobs for the specified repoID and runID. @@ -1674,7 +1664,7 @@ func GetWorkflowJob(ctx *context.APIContext) { } if !has || job.RepoID != ctx.Repo.Repository.ID { - ctx.APIErrorNotFound(util.ErrNotExist) + ctx.APIErrorNotFound() return } diff --git a/routers/api/v1/repo/actions_run.go b/routers/api/v1/repo/actions_run.go index d1d98ff21ab..1765ed564d6 100644 --- a/routers/api/v1/repo/actions_run.go +++ b/routers/api/v1/repo/actions_run.go @@ -4,10 +4,7 @@ package repo import ( - "errors" - actions_model "gitea.dev/models/actions" - "gitea.dev/modules/util" "gitea.dev/routers/common" "gitea.dev/services/context" ) @@ -45,11 +42,7 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) { jobID := ctx.PathParamInt64("job_id") curJob, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobID) if err != nil { - if errors.Is(err, util.ErrNotExist) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } if err = curJob.LoadRepo(ctx); err != nil { @@ -59,10 +52,6 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) { err = common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, curJob) if err != nil { - if errors.Is(err, util.ErrNotExist) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) } } diff --git a/routers/api/v1/repo/avatar.go b/routers/api/v1/repo/avatar.go index b3d52e16634..b00394ffcc5 100644 --- a/routers/api/v1/repo/avatar.go +++ b/routers/api/v1/repo/avatar.go @@ -44,7 +44,7 @@ func UpdateAvatar(ctx *context.APIContext) { content, err := base64.StdEncoding.DecodeString(form.Image) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } diff --git a/routers/api/v1/repo/blob.go b/routers/api/v1/repo/blob.go index cd731b0d787..79b2dee245c 100644 --- a/routers/api/v1/repo/blob.go +++ b/routers/api/v1/repo/blob.go @@ -48,7 +48,7 @@ func GetBlob(ctx *context.APIContext) { } if blob, err := files_service.GetBlobBySHA(ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil { - ctx.APIError(http.StatusBadRequest, err) + 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 629945398d8..0806858b4da 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -64,7 +64,7 @@ func GetBranch(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } else if !exist { - ctx.APIErrorNotFound(err) + ctx.APIErrorNotFound() return } @@ -153,11 +153,11 @@ func DeleteBranch(ctx *context.APIContext) { if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil { switch { case git.IsErrBranchNotExist(err): - ctx.APIErrorNotFound(err) + ctx.APIErrorNotFound() case errors.Is(err, repo_service.ErrBranchIsDefault): - ctx.APIError(http.StatusForbidden, errors.New("can not delete default or pull request target branch")) + ctx.APIError(http.StatusForbidden, "can not delete default or pull request target branch") case errors.Is(err, git_model.ErrBranchIsProtected): - ctx.APIError(http.StatusForbidden, errors.New("branch protected")) + ctx.APIError(http.StatusForbidden, "branch protected") default: ctx.APIErrorInternal(err) } @@ -306,6 +306,10 @@ func ListBranches(ctx *context.APIContext) { // in: query // description: page size of results // type: integer + // - name: q + // in: query + // description: branch name substring to filter by + // type: string // responses: // "200": // "$ref": "#/responses/BranchList" @@ -314,6 +318,7 @@ func ListBranches(ctx *context.APIContext) { var apiBranches []*api.Branch listOptions := utils.GetListOptions(ctx) + keyword := ctx.FormString("q") if !ctx.Repo.Repository.IsEmpty { if ctx.Repo.GitRepo == nil { @@ -325,6 +330,7 @@ func ListBranches(ctx *context.APIContext) { ListOptions: listOptions, RepoID: ctx.Repo.Repository.ID, IsDeletedBranch: optional.Some(false), + Keyword: keyword, } var err error totalNumOfBranches, err = db.Count[git_model.Branch](ctx, branchOpts) @@ -440,9 +446,9 @@ func UpdateBranch(ctx *context.APIContext) { if err := repo_service.UpdateBranch(ctx, repo, ctx.Repo.GitRepo, ctx.Doer, branchName, opt.NewCommitID, opt.OldCommitID, opt.Force); err != nil { switch { case git_model.IsErrBranchNotExist(err): - ctx.APIErrorNotFound(err) + ctx.APIErrorNotFound() case errors.Is(err, util.ErrInvalidArgument): - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) case git.IsErrPushRejected(err): rej := err.(*git.ErrPushRejected) ctx.APIError(http.StatusForbidden, rej.Message) @@ -678,7 +684,7 @@ func CreateBranchProtection(ctx *context.APIContext) { whitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -687,7 +693,7 @@ func CreateBranchProtection(ctx *context.APIContext) { forcePushAllowlistUsers, err := user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -696,7 +702,7 @@ func CreateBranchProtection(ctx *context.APIContext) { mergeWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -705,7 +711,7 @@ func CreateBranchProtection(ctx *context.APIContext) { approvalsWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -716,7 +722,7 @@ func CreateBranchProtection(ctx *context.APIContext) { bypassAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.BypassAllowlistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -728,7 +734,7 @@ func CreateBranchProtection(ctx *context.APIContext) { whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -737,7 +743,7 @@ func CreateBranchProtection(ctx *context.APIContext) { forcePushAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ForcePushAllowlistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -746,7 +752,7 @@ func CreateBranchProtection(ctx *context.APIContext) { mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.MergeWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -755,7 +761,7 @@ func CreateBranchProtection(ctx *context.APIContext) { approvalsWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ApprovalsWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -765,7 +771,7 @@ func CreateBranchProtection(ctx *context.APIContext) { bypassAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.BypassAllowlistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -993,7 +999,7 @@ func EditBranchProtection(ctx *context.APIContext) { whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1006,7 +1012,7 @@ func EditBranchProtection(ctx *context.APIContext) { forcePushAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1019,7 +1025,7 @@ func EditBranchProtection(ctx *context.APIContext) { mergeWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1032,7 +1038,7 @@ func EditBranchProtection(ctx *context.APIContext) { approvalsWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1045,7 +1051,7 @@ func EditBranchProtection(ctx *context.APIContext) { bypassAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.BypassAllowlistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1061,7 +1067,7 @@ func EditBranchProtection(ctx *context.APIContext) { whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1074,7 +1080,7 @@ func EditBranchProtection(ctx *context.APIContext) { forcePushAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ForcePushAllowlistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1087,7 +1093,7 @@ func EditBranchProtection(ctx *context.APIContext) { mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.MergeWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1100,7 +1106,7 @@ func EditBranchProtection(ctx *context.APIContext) { approvalsWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ApprovalsWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1113,7 +1119,7 @@ func EditBranchProtection(ctx *context.APIContext) { bypassAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.BypassAllowlistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1325,10 +1331,10 @@ func MergeUpstream(ctx *context.APIContext) { mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch, form.FfOnly) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) return } ctx.APIErrorInternal(err) diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index b7938cb516d..e254d5e1289 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -107,7 +107,7 @@ func IsCollaborator(ctx *context.APIContext) { user, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -167,7 +167,7 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) { collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -186,7 +186,7 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) { if err := repo_service.AddOrUpdateCollaborator(ctx, ctx.Repo.Repository, collaborator, p); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -230,7 +230,7 @@ func DeleteCollaborator(ctx *context.APIContext) { collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -284,7 +284,7 @@ func GetRepoPermissions(ctx *context.APIContext) { collaborator, err := user_model.GetUserByName(ctx, collaboratorUsername) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -326,7 +326,7 @@ func GetReviewers(ctx *context.APIContext) { canChooseReviewer := issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, ctx.Repo.Repository, 0) if !canChooseReviewer { - ctx.APIError(http.StatusForbidden, errors.New("doer has no permission to get reviewers")) + ctx.APIError(http.StatusForbidden, "doer has no permission to get reviewers") return } diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index 0e66bae4304..6f8aaefb6d9 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -217,7 +217,7 @@ func GetAllCommits(ctx *context.APIContext) { // get commit specified by sha baseCommit, err = ctx.Repo.GitRepo.GetCommit(sha) if err != nil { - ctx.NotFoundOrServerError(err) + ctx.APIErrorAuto(err) return } } @@ -258,11 +258,11 @@ func GetAllCommits(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } else if commitsCountTotal == 0 { - ctx.APIErrorNotFound("FileCommitsCount", nil) + ctx.APIErrorNotFound() return } - commits, err = ctx.Repo.GitRepo.CommitsByFileAndRange( + commits, _, err = ctx.Repo.GitRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ Revision: sha, File: path, @@ -383,7 +383,7 @@ func GetCommitPullRequest(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, ctx.PathParam("sha")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/download.go b/routers/api/v1/repo/download.go index d9470d143ea..0df406488c3 100644 --- a/routers/api/v1/repo/download.go +++ b/routers/api/v1/repo/download.go @@ -17,9 +17,9 @@ func serveRepoArchive(ctx *context.APIContext, reqFileName string, paths []strin aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, reqFileName, paths) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -28,7 +28,7 @@ func serveRepoArchive(ctx *context.APIContext, reqFileName string, paths []strin err = archiver_service.ServeRepoArchive(ctx.Base, aReq) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 5f0659dc818..5f10c4fbd71 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -213,7 +213,7 @@ func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEn } if entry.IsDir() || entry.IsSubModule() { - ctx.APIErrorNotFound("getBlobForEntry", nil) + ctx.APIErrorNotFound() return nil, nil, nil } @@ -301,18 +301,14 @@ func GetEditorconfig(ctx *context.APIContext) { ec, _, err := ctx.Repo.GetEditorconfig(ctx.Repo.Commit) if err != nil { - if git.IsErrNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } fileName := ctx.PathParam("filename") def, err := ec.GetDefinitionForFilename(fileName) - if def == nil { - ctx.APIErrorNotFound(err) + if err != nil { + ctx.APIErrorNotFound(err.Error()) return } ctx.JSON(http.StatusOK, def) @@ -409,7 +405,7 @@ func ChangeFiles(ctx *context.APIContext) { for _, file := range apiOpts.Files { contentReader, err := base64Reader(file.ContentBase64) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } // FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options @@ -483,7 +479,7 @@ func CreateFile(ctx *context.APIContext) { } contentReader, err := base64Reader(apiOpts.ContentBase64) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } @@ -554,7 +550,7 @@ func UpdateFile(ctx *context.APIContext) { } contentReader, err := base64Reader(apiOpts.ContentBase64) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } willCreate := apiOpts.SHA == "" @@ -584,17 +580,17 @@ func handleChangeRepoFilesError(ctx *context.APIContext, err error) { return } if files_service.IsErrUserCannotCommit(err) || pull_service.IsErrFilePathProtected(err) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) return } if git_model.IsErrBranchAlreadyExists(err) || files_service.IsErrFilenameInvalid(err) || pull_service.IsErrSHADoesNotMatch(err) || files_service.IsErrFilePathInvalid(err) || files_service.IsErrRepoFileAlreadyExists(err) || files_service.IsErrCommitIDDoesNotMatch(err) || files_service.IsErrSHAOrCommitIDNotProvided(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) return } ctx.APIErrorInternal(err) @@ -699,10 +695,8 @@ func DeleteFile(ctx *context.APIContext) { func resolveRefCommit(ctx *context.APIContext, ref string, minCommitIDLen ...int) *utils.RefCommit { ref = util.IfZero(ref, ctx.Repo.Repository.DefaultBranch) refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ref, minCommitIDLen...) - if errors.Is(err, util.ErrNotExist) { - ctx.APIErrorNotFound(err) - } else if err != nil { - ctx.APIErrorInternal(err) + if err != nil { + ctx.APIErrorAuto(err) } return refCommit } @@ -828,11 +822,8 @@ func getRepoContents(ctx *context.APIContext, opts files_service.GetContentsOrLi } ret, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts) if err != nil { - if git.IsErrNotExist(err) { - ctx.APIErrorNotFound("GetContentsOrList", err) - return nil - } - ctx.APIErrorInternal(err) + ctx.APIErrorAuto(err) + return nil } return &ret } diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index 9d95f538743..8943ad39931 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -165,9 +165,9 @@ func CreateFork(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) } else if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 5d73cc3b312..9946afc8b7f 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -187,7 +187,7 @@ func SearchIssues(ctx *context.APIContext) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } @@ -196,7 +196,7 @@ func SearchIssues(ctx *context.APIContext) { repoIDs, allPublic, err := buildSearchIssuesRepoIDs(ctx) if err != nil { if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -384,7 +384,7 @@ func ListIssues(ctx *context.APIContext) { // "$ref": "#/responses/notFound" before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } @@ -540,16 +540,10 @@ func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 { } user, err := user_model.GetUserByName(ctx, userName) - if user_model.IsErrUserNotExist(err) { - ctx.APIErrorNotFound(err) - return 0 - } - if err != nil { - ctx.APIErrorInternal(err) + ctx.APIErrorAuto(err) return 0 } - return user.ID } @@ -682,7 +676,7 @@ func CreateIssue(ctx *context.APIContext) { return } if !valid { - ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name}) + ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name}.Error()) return } } @@ -693,9 +687,9 @@ func CreateIssue(ctx *context.APIContext) { if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, form.Projects); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -794,7 +788,7 @@ func EditIssue(ctx *context.APIContext) { // handles concurrent requests. // TODO: wrap all mutations in a transaction to fully prevent partial writes. if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion { - ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged) + ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged.Error()) return } @@ -813,7 +807,7 @@ func EditIssue(ctx *context.APIContext) { err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion) if err != nil { if errors.Is(err, issues_model.ErrIssueAlreadyChanged) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } @@ -869,7 +863,7 @@ func EditIssue(ctx *context.APIContext) { err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer) if err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -918,7 +912,7 @@ func EditIssue(ctx *context.APIContext) { if canWrite && form.Projects != nil { if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, *form.Projects); err != nil { if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -969,11 +963,7 @@ func DeleteIssue(ctx *context.APIContext) { // "$ref": "#/responses/notFound" issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } diff --git a/routers/api/v1/repo/issue_attachment.go b/routers/api/v1/repo/issue_attachment.go index 00fa792771f..123f66399d4 100644 --- a/routers/api/v1/repo/issue_attachment.go +++ b/routers/api/v1/repo/issue_attachment.go @@ -194,9 +194,9 @@ func CreateIssueAttachment(ctx *context.APIContext) { }) if err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else if errors.Is(err, util.ErrContentTooLarge) { - ctx.APIError(http.StatusRequestEntityTooLarge, err) + ctx.APIError(http.StatusRequestEntityTooLarge, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -272,7 +272,7 @@ func EditIssueAttachment(ctx *context.APIContext) { if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attachment); err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -336,7 +336,7 @@ func DeleteIssueAttachment(ctx *context.APIContext) { func getIssueFromContext(ctx *context.APIContext) *issues_model.Issue { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - ctx.NotFoundOrServerError(err) + ctx.APIErrorAuto(err) return nil } @@ -361,7 +361,7 @@ func getIssueAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment func getIssueAttachmentSafeRead(ctx *context.APIContext, issue *issues_model.Issue) *repo_model.Attachment { attachment, err := repo_model.GetAttachmentByID(ctx, ctx.PathParamInt64("attachment_id")) if err != nil { - ctx.NotFoundOrServerError(err) + ctx.APIErrorAuto(err) return nil } if !attachmentBelongsToRepoOrIssue(ctx, attachment, issue) { diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index 5b2d2084735..6cece7e5cee 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -65,7 +65,7 @@ func ListIssueComments(ctx *context.APIContext) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) @@ -169,7 +169,7 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) @@ -274,7 +274,7 @@ func ListRepoIssueComments(ctx *context.APIContext) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } @@ -392,14 +392,14 @@ func CreateIssueComment(ctx *context.APIContext) { } if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { - ctx.APIError(http.StatusForbidden, errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked"))) + ctx.APIError(http.StatusForbidden, ctx.Locale.TrString("repo.issues.comment_on_locked")) return } comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil) if err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -447,11 +447,7 @@ func GetIssueComment(ctx *context.APIContext) { comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -572,11 +568,7 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) { func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) { comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -595,7 +587,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) comment.Content = form.Body if err := issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, oldContent); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -681,11 +673,7 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) { func deleteIssueComment(ctx *context.APIContext) { comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go index d2650edb634..56d494190e4 100644 --- a/routers/api/v1/repo/issue_comment_attachment.go +++ b/routers/api/v1/repo/issue_comment_attachment.go @@ -202,9 +202,9 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) { }) if err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else if errors.Is(err, util.ErrContentTooLarge) { - ctx.APIError(http.StatusRequestEntityTooLarge, err) + ctx.APIError(http.StatusRequestEntityTooLarge, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -218,7 +218,7 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) { if err = issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, comment.Content); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -285,7 +285,7 @@ func EditIssueCommentAttachment(ctx *context.APIContext) { if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attach); err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -346,7 +346,7 @@ func DeleteIssueCommentAttachment(ctx *context.APIContext) { func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment { comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { - ctx.NotFoundOrServerError(err) + ctx.APIErrorAuto(err) return nil } if err := comment.LoadIssue(ctx); err != nil { @@ -391,7 +391,7 @@ func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues func getIssueCommentAttachmentSafeRead(ctx *context.APIContext, comment *issues_model.Comment) *repo_model.Attachment { attachment, err := repo_model.GetAttachmentByID(ctx, ctx.PathParamInt64("attachment_id")) if err != nil { - ctx.NotFoundOrServerError(err) + ctx.APIErrorAuto(err) return nil } if !attachmentBelongsToRepoOrComment(ctx, attachment, comment) { diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go index 4912737c911..ff4e7cd5b46 100644 --- a/routers/api/v1/repo/issue_dependency.go +++ b/routers/api/v1/repo/issue_dependency.go @@ -63,11 +63,7 @@ func GetIssueDependencies(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound("IsErrIssueNotExist", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -487,11 +483,7 @@ func RemoveIssueBlocking(ctx *context.APIContext) { func getParamsIssue(ctx *context.APIContext) *issues_model.Issue { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound("IsErrIssueNotExist", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return nil } issue.Repo = ctx.Repo.Repository @@ -508,11 +500,7 @@ func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Is var err error repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, form.Owner, form.Name) if err != nil { - if repo_model.IsErrRepoNotExist(err) { - ctx.APIErrorNotFound("IsErrRepoNotExist", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return nil } } else { @@ -521,11 +509,7 @@ func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Is issue, err := issues_model.GetIssueByIndex(ctx, repo.ID, form.Index) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound("IsErrIssueNotExist", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return nil } issue.Repo = repo diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go index ecf42688615..c1bbbe29d00 100644 --- a/routers/api/v1/repo/issue_label.go +++ b/routers/api/v1/repo/issue_label.go @@ -181,7 +181,7 @@ func DeleteIssueLabel(ctx *context.APIContext) { label, err := issues_model.GetLabelByID(ctx, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrLabelNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/issue_lock.go b/routers/api/v1/repo/issue_lock.go index 283b441fd20..2a4f75a9373 100644 --- a/routers/api/v1/repo/issue_lock.go +++ b/routers/api/v1/repo/issue_lock.go @@ -4,7 +4,6 @@ package repo import ( - "errors" "net/http" issues_model "gitea.dev/models/issues" @@ -54,16 +53,12 @@ func LockIssue(ctx *context.APIContext) { reason := web.GetForm(ctx).(*api.LockIssueOption).Reason issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { - ctx.APIError(http.StatusForbidden, errors.New("no permission to lock this issue")) + ctx.APIError(http.StatusForbidden, "no permission to lock this issue") return } @@ -121,16 +116,12 @@ func UnlockIssue(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { - ctx.APIError(http.StatusForbidden, errors.New("no permission to unlock this issue")) + ctx.APIError(http.StatusForbidden, "no permission to unlock this issue") return } diff --git a/routers/api/v1/repo/issue_pin.go b/routers/api/v1/repo/issue_pin.go index b8cebfbb04e..c71649aeca4 100644 --- a/routers/api/v1/repo/issue_pin.go +++ b/routers/api/v1/repo/issue_pin.go @@ -46,7 +46,7 @@ func PinIssue(ctx *context.APIContext) { if issues_model.IsErrIssueNotExist(err) { ctx.APIErrorNotFound() } else if issues_model.IsErrIssueMaxPinReached(err) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go index 3662e63c71b..6ad44ead619 100644 --- a/routers/api/v1/repo/issue_reaction.go +++ b/routers/api/v1/repo/issue_reaction.go @@ -53,11 +53,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) { comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -72,7 +68,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) { } if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) { - ctx.APIError(http.StatusForbidden, errors.New("no permission to get reactions")) + ctx.APIError(http.StatusForbidden, "no permission to get reactions") return } @@ -190,11 +186,7 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) { func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) { comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -214,7 +206,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp } if comment.Issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull) { - ctx.APIError(http.StatusForbidden, errors.New("no permission to change reaction")) + ctx.APIError(http.StatusForbidden, "no permission to change reaction") return } @@ -223,7 +215,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Reaction) if err != nil { if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else if issues_model.IsErrReactionAlreadyExist(err) { ctx.JSON(http.StatusOK, api.Reaction{ User: convert.ToUser(ctx, ctx.Doer, ctx.Doer), @@ -305,7 +297,7 @@ func GetIssueReactions(ctx *context.APIContext) { } if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) { - ctx.APIError(http.StatusForbidden, errors.New("no permission to get reactions")) + ctx.APIError(http.StatusForbidden, "no permission to get reactions") return } @@ -429,7 +421,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i } if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { - ctx.APIError(http.StatusForbidden, errors.New("no permission to change reaction")) + ctx.APIError(http.StatusForbidden, "no permission to change reaction") return } @@ -438,7 +430,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction) if err != nil { if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else if issues_model.IsErrReactionAlreadyExist(err) { ctx.JSON(http.StatusOK, api.Reaction{ User: convert.ToUser(ctx, ctx.Doer, ctx.Doer), diff --git a/routers/api/v1/repo/issue_subscription.go b/routers/api/v1/repo/issue_subscription.go index b2480e88a5a..84af3194df2 100644 --- a/routers/api/v1/repo/issue_subscription.go +++ b/routers/api/v1/repo/issue_subscription.go @@ -128,7 +128,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) { // only admin and user for itself can change subscription if user.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin { - ctx.APIError(http.StatusForbidden, fmt.Errorf("%s is not permitted to change subscriptions for %s", ctx.Doer.Name, user.Name)) + ctx.APIError(http.StatusForbidden, fmt.Sprintf("%s is not permitted to change subscriptions for %s", ctx.Doer.Name, user.Name)) return } diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index 414b44f5246..33af841fbd1 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -4,7 +4,6 @@ package repo import ( - "errors" "net/http" "time" @@ -72,16 +71,12 @@ func ListTrackedTimes(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) { - ctx.APIErrorNotFound("Timetracker is disabled") + ctx.APIErrorNotFound("timetracker is disabled") return } issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -95,7 +90,7 @@ func ListTrackedTimes(ctx *context.APIContext) { if qUser != "" { user, err := user_model.GetUserByName(ctx, qUser) if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else if err != nil { ctx.APIErrorInternal(err) return @@ -104,7 +99,7 @@ func ListTrackedTimes(ctx *context.APIContext) { } if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } @@ -116,7 +111,7 @@ func ListTrackedTimes(ctx *context.APIContext) { if opts.UserID == 0 { opts.UserID = ctx.Doer.ID } else { - ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights")) + ctx.APIError(http.StatusForbidden, "query by user not allowed; not enough rights") return } } @@ -183,11 +178,7 @@ func AddTime(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.AddTimeOption) issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -266,11 +257,7 @@ func ResetIssueTime(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -286,7 +273,7 @@ func ResetIssueTime(ctx *context.APIContext) { err = issues_model.DeleteIssueUserTimes(ctx, issue, ctx.Doer) if err != nil { if db.IsErrNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -339,11 +326,7 @@ func DeleteTime(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -358,11 +341,7 @@ func DeleteTime(ctx *context.APIContext) { time, err := issues_model.GetTrackedTimeByID(ctx, issue.ID, ctx.PathParamInt64("id")) if err != nil { - if db.IsErrNotExist(err) { - ctx.APIErrorNotFound(err) - return - } - ctx.APIErrorInternal(err) + ctx.APIErrorAuto(err) return } if time.Deleted { @@ -424,11 +403,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) { } user, err := user_model.GetUserByName(ctx, ctx.PathParam("timetrackingusername")) if err != nil { - if user_model.IsErrUserNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } if user == nil { @@ -437,7 +412,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) { } if !ctx.IsUserRepoAdmin() && !ctx.Doer.IsAdmin && ctx.Doer.ID != user.ID { - ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights")) + ctx.APIError(http.StatusForbidden, "query by user not allowed; not enough rights") return } @@ -523,7 +498,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { if qUser != "" { user, err := user_model.GetUserByName(ctx, qUser) if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else if err != nil { ctx.APIErrorInternal(err) return @@ -533,7 +508,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { var err error if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } @@ -545,7 +520,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { if opts.UserID == 0 { opts.UserID = ctx.Doer.ID } else { - ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights")) + ctx.APIError(http.StatusForbidden, "query by user not allowed; not enough rights") return } } @@ -607,7 +582,7 @@ func ListMyTrackedTimes(ctx *context.APIContext) { var err error if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go index 44471a4f73f..b704bcee1d4 100644 --- a/routers/api/v1/repo/key.go +++ b/routers/api/v1/repo/key.go @@ -179,7 +179,7 @@ func HandleCheckKeyStringError(ctx *context.APIContext, err error) { } else if asymkey_model.IsErrKeyUnableVerify(err) { ctx.APIError(http.StatusUnprocessableEntity, "Unable to verify key content") } else { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid key content: %w", err)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid key content: %v", err)) } } diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go index aec82dce757..790a9212bc2 100644 --- a/routers/api/v1/repo/label.go +++ b/routers/api/v1/repo/label.go @@ -153,7 +153,7 @@ func CreateLabel(ctx *context.APIContext) { color, err := label.NormalizeColor(form.Color) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } form.Color = color @@ -231,7 +231,7 @@ func EditLabel(ctx *context.APIContext) { if form.Color != nil { color, err := label.NormalizeColor(*form.Color) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } l.Color = color diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index 65ef10777c4..0e3e68d1e8b 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -72,7 +72,7 @@ func Migrate(ctx *context.APIContext) { } if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -110,12 +110,12 @@ func Migrate(ctx *context.APIContext) { gitServiceType := convert.ToGitServiceType(form.Service) if form.Mirror && setting.Mirror.DisableNewPull { - ctx.APIError(http.StatusForbidden, errors.New("the site administrator has disabled the creation of new pull mirrors")) + ctx.APIError(http.StatusForbidden, "the site administrator has disabled the creation of new pull mirrors") return } if setting.Repository.DisableMigrations { - ctx.APIError(http.StatusForbidden, errors.New("the site administrator has disabled migrations")) + ctx.APIError(http.StatusForbidden, "the site administrator has disabled migrations") return } @@ -235,9 +235,9 @@ func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, err case db.IsErrNamePatternNotAllowed(err): ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(db.ErrNamePatternNotAllowed).Pattern)) case git.IsErrInvalidCloneAddr(err): - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) case base.IsErrNotSupported(err): - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) default: err = util.SanitizeErrorCredentialURLs(err) if strings.Contains(err.Error(), "Authentication failed") || diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go index 53d47ff135e..c76946493ab 100644 --- a/routers/api/v1/repo/mirror.go +++ b/routers/api/v1/repo/mirror.go @@ -5,6 +5,7 @@ package repo import ( "errors" + "fmt" "net/http" "strings" "time" @@ -112,7 +113,7 @@ func PushMirrorSync(ctx *context.APIContext) { // Get All push mirrors of a specific repo pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, ctx.Repo.Repository.ID, db.ListOptions{}) if err != nil { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) return } @@ -175,7 +176,7 @@ func ListPushMirrors(ctx *context.APIContext) { // Get all push mirrors for the specified repository. pushMirrors, count, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, utils.GetListOptions(ctx)) if err != nil { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) return } @@ -239,7 +240,7 @@ func GetPushMirrorByName(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } else if !exist { - ctx.APIError(http.StatusNotFound, nil) + ctx.APIErrorNotFound() return } @@ -334,7 +335,7 @@ func DeletePushMirrorByRemoteName(ctx *context.APIContext) { // Delete push mirror on repo by name. err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{RepoID: ctx.Repo.Repository.ID, RemoteName: remoteName}) if err != nil { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) return } ctx.Status(http.StatusNoContent) @@ -344,8 +345,12 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro repo := ctx.Repo.Repository interval, err := time.ParseDuration(mirrorOption.Interval) - if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { - ctx.APIError(http.StatusBadRequest, err) + if err != nil { + ctx.APIError(http.StatusBadRequest, fmt.Sprintf("invalid interval: %v", err)) + return + } + if interval != 0 && interval < setting.Mirror.MinInterval { + ctx.APIError(http.StatusBadRequest, fmt.Sprintf("interval is shorter than minimum %v", setting.Mirror.MinInterval.String())) return } diff --git a/routers/api/v1/repo/notes.go b/routers/api/v1/repo/notes.go index d8fea56c4cd..d2bd708aa48 100644 --- a/routers/api/v1/repo/notes.go +++ b/routers/api/v1/repo/notes.go @@ -68,11 +68,7 @@ func getNote(ctx *context.APIContext, identifier string) { commitID, err := ctx.Repo.GitRepo.ConvertToGitID(identifier) if err != nil { - if git.IsErrNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 123c0c671a9..dcfaa3d3ac8 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -126,7 +126,7 @@ func ListPullRequests(ctx *context.APIContext) { poster, err := user_model.GetUserByName(ctx, posterStr) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -448,7 +448,7 @@ func CreatePullRequest(ctx *context.APIContext) { HeadBranch: existingPr.HeadBranch, BaseBranch: existingPr.BaseBranch, } - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } @@ -551,7 +551,7 @@ func CreatePullRequest(ctx *context.APIContext) { return } if !valid { - ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}) + ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}.Error()) return } } @@ -570,11 +570,11 @@ func CreatePullRequest(ctx *context.APIContext) { if err := pull_service.NewPullRequest(ctx, prOpts); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else if errors.Is(err, issues_model.ErrMustCollaborator) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -663,7 +663,7 @@ func EditPullRequest(ctx *context.APIContext) { // handles concurrent requests. // TODO: wrap all mutations in a transaction to fully prevent partial writes. if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion { - ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged) + ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged.Error()) return } @@ -682,7 +682,7 @@ func EditPullRequest(ctx *context.APIContext) { err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion) if err != nil { if errors.Is(err, issues_model.ErrIssueAlreadyChanged) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } @@ -721,7 +721,7 @@ func EditPullRequest(ctx *context.APIContext) { if user_model.IsErrUserNotExist(err) { ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: [name: %s]", err)) } else if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -788,18 +788,18 @@ func EditPullRequest(ctx *context.APIContext) { return } if !branchExist { - ctx.APIError(http.StatusNotFound, fmt.Errorf("new base '%s' not exist", form.Base)) + ctx.APIError(http.StatusNotFound, fmt.Sprintf("new base '%s' not exist", form.Base)) return } if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, form.Base); err != nil { if issues_model.IsErrPullRequestAlreadyExists(err) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } else if issues_model.IsErrIssueIsClosed(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } else if pull_service.IsErrPullRequestHasMerged(err) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } ctx.APIErrorInternal(err) @@ -927,11 +927,7 @@ func MergePullRequest(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrPullRequestNotExist(err) { - ctx.APIErrorNotFound("GetPullRequestByIndex", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -977,11 +973,11 @@ func MergePullRequest(ctx *context.APIContext) { } else if errors.Is(err, pull_service.ErrNotMergeableState) { ctx.APIError(http.StatusMethodNotAllowed, "Please try again later") } else if errors.Is(err, pull_service.ErrNotReadyToMerge) { - ctx.APIError(http.StatusMethodNotAllowed, err) + ctx.APIError(http.StatusMethodNotAllowed, err.Error()) } else if asymkey_service.IsErrWontSign(err) { - ctx.APIError(http.StatusMethodNotAllowed, err) + ctx.APIError(http.StatusMethodNotAllowed, err.Error()) } else if errors.Is(err, pull_service.ErrHeadCommitsNotAllVerified) { - ctx.APIError(http.StatusMethodNotAllowed, err) + ctx.APIError(http.StatusMethodNotAllowed, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -992,11 +988,11 @@ func MergePullRequest(ctx *context.APIContext) { if manuallyMerged { if err := pull_service.MergedManually(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, form.MergeCommitID); err != nil { if pull_service.IsErrInvalidMergeStyle(err) { - ctx.APIError(http.StatusMethodNotAllowed, fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) + ctx.APIError(http.StatusMethodNotAllowed, fmt.Sprintf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) return } if strings.Contains(err.Error(), "Wrong commit ID") { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } ctx.APIErrorInternal(err) @@ -1034,7 +1030,7 @@ func MergePullRequest(ctx *context.APIContext) { scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, deleteBranchAfterMerge) if err != nil { if pull_model.IsErrAlreadyScheduledToAutoMerge(err) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } ctx.APIErrorInternal(err) @@ -1048,7 +1044,7 @@ func MergePullRequest(ctx *context.APIContext) { if err := pull_service.Merge(ctx, pr, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil { if pull_service.IsErrInvalidMergeStyle(err) { - ctx.APIError(http.StatusMethodNotAllowed, fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) + ctx.APIError(http.StatusMethodNotAllowed, fmt.Sprintf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) } else if pull_service.IsErrMergeConflicts(err) { conflictError := err.(pull_service.ErrMergeConflicts) ctx.JSON(http.StatusConflict, conflictError) @@ -1230,7 +1226,7 @@ func UpdatePullRequest(ctx *context.APIContext) { } if pr.HasMerged { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, "pull request is already merged") return } @@ -1240,7 +1236,7 @@ func UpdatePullRequest(ctx *context.APIContext) { } if pr.Issue.IsClosed { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, "pull request is already closed") return } @@ -1435,7 +1431,7 @@ func GetPullRequestCommits(ctx *context.APIContext) { compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefNameFromBranch(pr.BaseBranch), git.RefName(pr.GetGitHeadRefName()), false, false) } - if gitcmd.StderrHasPrefix(err, "fatal: bad revision") { + if gitcmd.IsStderr(err, gitcmd.StderrBadRevision) { ctx.APIError(http.StatusNotFound, "invalid base branch or revision") return } else if err != nil { diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 901e6b69c08..9778dc416ac 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -63,11 +63,7 @@ func ListPullReviews(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrPullRequestNotExist(err) { - ctx.APIErrorNotFound("GetPullRequestByIndex", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -389,11 +385,7 @@ func updatePullReviewCommentResolve(ctx *context.APIContext, isResolve bool) { func getPullReviewCommentToResolve(ctx *context.APIContext) *issues_model.Comment { comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.APIErrorNotFound("GetCommentByID", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return nil } @@ -458,7 +450,7 @@ func DeletePullReview(ctx *context.APIContext) { return } if !ctx.Doer.IsAdmin && ctx.Doer.ID != review.ReviewerID { - ctx.APIError(http.StatusForbidden, nil) + ctx.APIError(http.StatusForbidden, "no permission to delete comment") return } @@ -510,11 +502,7 @@ func CreatePullReview(ctx *context.APIContext) { opts := web.GetForm(ctx).(*api.CreatePullReviewOptions) pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrPullRequestNotExist(err) { - ctx.APIErrorNotFound("GetPullRequestByIndex", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -575,7 +563,7 @@ func CreatePullReview(ctx *context.APIContext) { review, _, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil) if err != nil { if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -641,7 +629,7 @@ func SubmitPullReview(ctx *context.APIContext) { } if review.Type != issues_model.ReviewTypePending { - ctx.APIError(http.StatusUnprocessableEntity, errors.New("only a pending review can be submitted")) + ctx.APIError(http.StatusUnprocessableEntity, "only a pending review can be submitted") return } @@ -653,7 +641,7 @@ func SubmitPullReview(ctx *context.APIContext) { // if review stay pending return if reviewType == issues_model.ReviewTypePending { - ctx.APIError(http.StatusUnprocessableEntity, errors.New("review stay pending")) + ctx.APIError(http.StatusUnprocessableEntity, "review stay pending") return } @@ -667,7 +655,7 @@ func SubmitPullReview(ctx *context.APIContext) { review, _, err = pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil) if err != nil { if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -698,7 +686,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest case api.ReviewStateApproved: // can not approve your own PR if pr.Issue.IsPoster(ctx.Doer.ID) { - ctx.APIError(http.StatusUnprocessableEntity, errors.New("approve your own pull is not allowed")) + ctx.APIError(http.StatusUnprocessableEntity, "approve your own pull is not allowed") return -1, true } reviewType = issues_model.ReviewTypeApprove @@ -707,7 +695,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest case api.ReviewStateRequestChanges: // can not reject your own PR if pr.Issue.IsPoster(ctx.Doer.ID) { - ctx.APIError(http.StatusUnprocessableEntity, errors.New("reject your own pull is not allowed")) + ctx.APIError(http.StatusUnprocessableEntity, "reject your own pull is not allowed") return -1, true } reviewType = issues_model.ReviewTypeReject @@ -717,7 +705,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest needsBody = false // if there is no body we need to ensure that there are comments if !hasBody && !hasComments { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("review event %s requires a body or a comment", event)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("review event %s requires a body or a comment", event)) return -1, true } default: @@ -726,7 +714,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest // reject reviews with empty body if a body is required for this call if needsBody && !hasBody { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("review event %s requires a body", event)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("review event %s requires a body", event)) return -1, true } @@ -737,33 +725,25 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues_model.PullRequest, bool) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrPullRequestNotExist(err) { - ctx.APIErrorNotFound("GetPullRequestByIndex", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return nil, nil, true } review, err := issues_model.GetReviewByID(ctx, ctx.PathParamInt64("id")) if err != nil { - if issues_model.IsErrReviewNotExist(err) { - ctx.APIErrorNotFound("GetReviewByID", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return nil, nil, true } // validate the review is for the given PR if review.IssueID != pr.IssueID { - ctx.APIErrorNotFound("ReviewNotInPR") + ctx.APIErrorNotFound() return nil, nil, true } // make sure that the user has access to this review if it is pending if review.Type == issues_model.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.Doer.IsAdmin { - ctx.APIErrorNotFound("GetReviewByID") + ctx.APIErrorNotFound() return nil, nil, true } @@ -870,7 +850,7 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIErrorNotFound("UserNotExist", fmt.Sprintf("User '%s' not exist", r)) + ctx.APIErrorNotFound("user doesn't exist: " + r) return nil, nil } ctx.APIErrorInternal(err) @@ -886,7 +866,7 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN teamReviewer, err = organization.GetTeam(ctx, ctx.Repo.Owner.ID, t) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIErrorNotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t)) + ctx.APIErrorNotFound("team doesn't exist: " + t) return nil, nil } ctx.APIErrorInternal(err) @@ -902,11 +882,7 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrPullRequestNotExist(err) { - ctx.APIErrorNotFound("GetPullRequestByIndex", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -935,11 +911,11 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, &permDoer, reviewer, isAdd) if err != nil { if issues_model.IsErrReviewRequestOnClosedPR(err) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) return } if issues_model.IsErrNotValidReviewRequest(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -960,11 +936,11 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions comment, err := issue_service.TeamReviewRequest(ctx, pr.Issue, ctx.Doer, teamReviewer, isAdd) if err != nil { if issues_model.IsErrReviewRequestOnClosedPR(err) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) return } if issues_model.IsErrNotValidReviewRequest(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1102,7 +1078,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors _, err := pull_service.DismissReview(ctx, review.ID, ctx.Repo.Repository.ID, msg, ctx.Doer, isDismiss, dismissPriors) if err != nil { if pull_service.IsErrDismissRequestOnClosedPR(err) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) return } ctx.APIErrorInternal(err) diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index ee995f4402b..a4fc03ae321 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -4,8 +4,6 @@ package repo import ( - "errors" - "fmt" "net/http" auth_model "gitea.dev/models/auth" @@ -243,7 +241,7 @@ func CreateRelease(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateReleaseOption) if ctx.Repo.Repository.IsEmpty { - ctx.APIError(http.StatusUnprocessableEntity, errors.New("repo is empty")) + ctx.APIError(http.StatusUnprocessableEntity, "repo is empty") return } rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName) @@ -273,11 +271,11 @@ func CreateRelease(ctx *context.APIContext) { // 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 repo_model.IsErrReleaseAlreadyExist(err) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) } else if release_service.IsErrProtectedTagName(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else if git.IsErrNotExist(err) { - ctx.APIError(http.StatusNotFound, fmt.Errorf("target \"%v\" not found: %w", rel.Target, err)) + ctx.APIError(http.StatusNotFound, "target not found") } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go index 896c6d24ccb..915ac725b06 100644 --- a/routers/api/v1/repo/release_attachment.go +++ b/routers/api/v1/repo/release_attachment.go @@ -205,7 +205,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) { // Check if attachments are enabled if !setting.Attachment.Enabled { - ctx.APIErrorNotFound("Attachment is not enabled") + ctx.APIErrorNotFound("attachment is not enabled") return } @@ -250,12 +250,12 @@ func CreateReleaseAttachment(ctx *context.APIContext) { }) if err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } if errors.Is(err, util.ErrContentTooLarge) { - ctx.APIError(http.StatusRequestEntityTooLarge, err) + ctx.APIError(http.StatusRequestEntityTooLarge, err.Error()) return } @@ -340,7 +340,7 @@ func EditReleaseAttachment(ctx *context.APIContext) { if err := attachment_service.UpdateAttachment(ctx, setting.Repository.Release.AllowedTypes, attach); err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index e0e40086a56..8adef610001 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -173,7 +173,7 @@ func Search(ctx *context.APIContext) { opts.Collaborate = optional.Some(true) case "": default: - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid search mode: \"%s\"", mode)) + ctx.APIError(http.StatusUnprocessableEntity, "invalid search mode") return } @@ -234,7 +234,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre // If the readme template does not exist, a 400 will be returned. if opt.AutoInit && len(opt.Readme) > 0 && !slices.Contains(repo_module.Readmes, opt.Readme) { - ctx.APIError(http.StatusBadRequest, fmt.Errorf("readme template does not exist, available templates: %v", repo_module.Readmes)) + ctx.APIError(http.StatusBadRequest, fmt.Sprintf("readme template does not exist, available templates: %v", repo_module.Readmes)) return } @@ -258,9 +258,9 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre } else if db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || label.IsErrTemplateLoad(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else if errors.Is(err, util.ErrPermissionDenied) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -413,7 +413,7 @@ func Generate(ctx *context.APIContext) { ctx.APIError(http.StatusConflict, "The repository with the same name already exists.") } else if db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -652,13 +652,13 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil { switch { case repo_model.IsErrRepoAlreadyExist(err): - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) case db.IsErrNameReserved(err): - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) case db.IsErrNamePatternNotAllowed(err): - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) default: - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("ChangeRepositoryName: %w", err)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("ChangeRepositoryName: %v", err)) } return err } @@ -692,7 +692,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.Doer.IsAdmin { err := errors.New("cannot change private repository to public") - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return err } @@ -756,12 +756,12 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { // Check that values are valid if !validation.IsValidURL(opts.ExternalTracker.ExternalTrackerURL) { err := errors.New("External tracker URL not valid") - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return err } if len(opts.ExternalTracker.ExternalTrackerFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) { err := errors.New("External tracker URL format not valid") - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return err } @@ -897,7 +897,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { // so unrelated PATCH calls don't reject historical configs. if opts.AllowMergeUpdate != nil || opts.AllowRebaseUpdate != nil || opts.DefaultUpdateStyle != nil { if err := config.ValidateUpdateSettings(); err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return err } } @@ -988,7 +988,7 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e if opts.Archived != nil { if repo.IsMirror { err := errors.New("repo is a mirror, cannot archive/un-archive") - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return err } if *opts.Archived { @@ -1042,14 +1042,14 @@ func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error { interval, err := time.ParseDuration(*opts.MirrorInterval) if err != nil { log.Error("Wrong format for MirrorInternal Sent: %s", err) - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return err } // Ensure the provided duration is not too short if interval != 0 && interval < setting.Mirror.MinInterval { err := fmt.Errorf("invalid mirror interval: %s is below minimum interval: %s", interval, setting.Mirror.MinInterval) - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return err } @@ -1119,7 +1119,7 @@ func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error { // finally update the mirror in the DB if err := repo_model.UpdateMirror(ctx, mirror); err != nil { log.Error("Failed to Set Mirror Interval: %s", err) - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return err } diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go index 9df2a32e72d..c7d7014e541 100644 --- a/routers/api/v1/repo/status.go +++ b/routers/api/v1/repo/status.go @@ -55,7 +55,7 @@ func NewCommitStatus(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateStatusOption) sha := ctx.PathParam("sha") if len(sha) == 0 { - ctx.APIError(http.StatusBadRequest, nil) + ctx.APIError(http.StatusBadRequest, "sha not provided") return } status := &git_model.CommitStatus{ diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go index 796bdb10883..d43d5ea628b 100644 --- a/routers/api/v1/repo/tag.go +++ b/routers/api/v1/repo/tag.go @@ -109,13 +109,13 @@ func GetAnnotatedTag(ctx *context.APIContext) { tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } commit, err := ctx.Repo.GitRepo.GetTagCommit(tag.Name) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit)) @@ -203,13 +203,13 @@ func CreateTag(ctx *context.APIContext) { commit, err := ctx.Repo.GitRepo.GetCommit(form.Target) if err != nil { - ctx.APIError(http.StatusNotFound, fmt.Errorf("target not found: %w", err)) + ctx.APIError(http.StatusNotFound, fmt.Sprintf("target not found: %v", err)) return } if err := release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, commit.ID.String(), form.TagName, form.Message); err != nil { if release_service.IsErrTagAlreadyExists(err) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } if release_service.IsErrProtectedTagName(err) { @@ -278,7 +278,7 @@ func DeleteTag(ctx *context.APIContext) { } if !tag.IsTag { - ctx.APIError(http.StatusConflict, errors.New("a tag attached to a release cannot be deleted directly")) + ctx.APIError(http.StatusConflict, "a tag attached to a release cannot be deleted directly") return } @@ -438,7 +438,7 @@ func CreateTagProtection(ctx *context.APIContext) { whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -449,7 +449,7 @@ func CreateTagProtection(ctx *context.APIContext) { whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -546,7 +546,7 @@ func EditTagProtection(ctx *context.APIContext) { whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -560,7 +560,7 @@ func EditTagProtection(ctx *context.APIContext) { whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) diff --git a/routers/api/v1/repo/teams.go b/routers/api/v1/repo/teams.go index 1913bb6eecd..fc39b1c6166 100644 --- a/routers/api/v1/repo/teams.go +++ b/routers/api/v1/repo/teams.go @@ -201,13 +201,13 @@ func changeRepoTeam(ctx *context.APIContext, add bool) { var err error if add { if repoHasTeam { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team '%s' is already added to repo", team.Name)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("team '%s' is already added to repo", team.Name)) return } err = repo_service.TeamAddRepository(ctx, team, ctx.Repo.Repository) } else { if !repoHasTeam { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team '%s' was not added to repo", team.Name)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("team '%s' was not added to repo", team.Name)) return } err = repo_service.RemoveRepositoryFromTeam(ctx, team, ctx.Repo.Repository.ID) @@ -224,7 +224,7 @@ func getTeamByParam(ctx *context.APIContext) *organization.Team { team, err := organization.GetTeam(ctx, ctx.Repo.Owner.ID, ctx.PathParam("team")) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) return nil } ctx.APIErrorInternal(err) diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go index e2c124a86a5..63fc3b0712c 100644 --- a/routers/api/v1/repo/transfer.go +++ b/routers/api/v1/repo/transfer.go @@ -87,12 +87,12 @@ func Transfer(ctx *context.APIContext) { for _, tID := range *opts.TeamIDs { team, err := organization.GetTeamByID(ctx, tID) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team %d not found", tID)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("team %d not found", tID)) return } if team.OrgID != org.ID { - ctx.APIError(http.StatusForbidden, fmt.Errorf("team %d belongs not to org %d", tID, org.ID)) + ctx.APIError(http.StatusForbidden, fmt.Sprintf("team %d belongs not to org %d", tID, org.ID)) return } @@ -110,13 +110,13 @@ func Transfer(ctx *context.APIContext) { if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil { switch { case repo_model.IsErrRepoTransferInProgress(err): - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) case repo_model.IsErrRepoAlreadyExist(err): - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) case repo_service.IsRepositoryLimitReached(err): - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) case errors.Is(err, user_model.ErrBlockedUser): - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) default: ctx.APIErrorInternal(err) } @@ -163,11 +163,11 @@ func AcceptTransfer(ctx *context.APIContext) { if err != nil { switch { case repo_model.IsErrNoPendingTransfer(err): - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) case errors.Is(err, util.ErrPermissionDenied): - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) case repo_service.IsRepositoryLimitReached(err): - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) default: ctx.APIErrorInternal(err) } @@ -207,9 +207,9 @@ func RejectTransfer(ctx *context.APIContext) { if err != nil { switch { case repo_model.IsErrNoPendingTransfer(err): - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) case errors.Is(err, util.ErrPermissionDenied): - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) default: ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index e6fdd6afc60..67209fa1271 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -59,7 +59,7 @@ func NewWikiPage(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateWikiPageOptions) if util.IsEmptyString(form.Title) { - ctx.APIError(http.StatusBadRequest, nil) + ctx.APIError(http.StatusBadRequest, "title is required") return } @@ -71,16 +71,16 @@ func NewWikiPage(ctx *context.APIContext) { content, err := base64.StdEncoding.DecodeString(form.ContentBase64) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } form.ContentBase64 = string(content) if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.ContentBase64, form.Message); err != nil { if repo_model.IsErrWikiReservedName(err) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if repo_model.IsErrWikiAlreadyExist(err) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -149,7 +149,7 @@ func EditWikiPage(ctx *context.APIContext) { content, err := base64.StdEncoding.DecodeString(form.ContentBase64) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } form.ContentBase64 = string(content) @@ -245,11 +245,7 @@ func DeleteWikiPage(ctx *context.APIContext) { wikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName")) if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil { - if err.Error() == "file does not exist" { - ctx.APIErrorNotFound(err) - return - } - ctx.APIErrorInternal(err) + ctx.APIErrorAuto(err) return } @@ -435,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( git.CommitsByFileAndRangeOptions{ Revision: ctx.Repo.Repository.DefaultWikiBranch, File: pageFilename, @@ -474,21 +470,13 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) { wikiRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo()) if err != nil { - if git.IsErrNotExist(err) || err.Error() == "no such file or directory" { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return nil, nil } commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch) if err != nil { - if git.IsErrNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return wikiRepo, nil } return wikiRepo, commit diff --git a/routers/api/v1/shared/action.go b/routers/api/v1/shared/action.go index 3dbe7008bec..5c95fa192c0 100644 --- a/routers/api/v1/shared/action.go +++ b/routers/api/v1/shared/action.go @@ -16,6 +16,7 @@ import ( "gitea.dev/modules/optional" "gitea.dev/modules/setting" api "gitea.dev/modules/structs" + "gitea.dev/modules/util" "gitea.dev/modules/webhook" "gitea.dev/routers/api/v1/utils" "gitea.dev/services/context" @@ -53,7 +54,7 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64, runAttemptI for _, status := range ctx.FormStrings("status") { values, err := convertToInternal(status) if err != nil { - ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status)) + ctx.APIError(http.StatusBadRequest, err.Error()) return } opts.Statuses = append(opts.Statuses, values...) @@ -125,7 +126,7 @@ func convertToInternal(s string) ([]actions_model.Status, error) { case "cancelled", "timed_out": return []actions_model.Status{actions_model.StatusCancelled}, nil default: - return nil, fmt.Errorf("invalid status %s", s) + return nil, util.NewInvalidArgumentErrorf("invalid status %s", s) } } @@ -155,7 +156,7 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) { for _, status := range ctx.FormStrings("status") { values, err := convertToInternal(status) if err != nil { - ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status)) + ctx.APIError(http.StatusBadRequest, err.Error()) return } opts.Status = append(opts.Status, values...) diff --git a/routers/api/v1/shared/block.go b/routers/api/v1/shared/block.go index e8190041aa5..c2a5fe8a4ef 100644 --- a/routers/api/v1/shared/block.go +++ b/routers/api/v1/shared/block.go @@ -45,7 +45,7 @@ func ListBlocks(ctx *context.APIContext, blocker *user_model.User) { func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) { blockee, err := user_model.GetUserByName(ctx, ctx.PathParam("username")) if err != nil { - ctx.APIErrorNotFound("GetUserByName", err) + ctx.APIErrorAuto(err) return } @@ -62,13 +62,13 @@ func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) { func BlockUser(ctx *context.APIContext, blocker *user_model.User) { blockee, err := user_model.GetUserByName(ctx, ctx.PathParam("username")) if err != nil { - ctx.APIErrorNotFound("GetUserByName", err) + ctx.APIErrorAuto(err) return } if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, ctx.FormString("note")); err != nil { if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -81,13 +81,13 @@ func BlockUser(ctx *context.APIContext, blocker *user_model.User) { func UnblockUser(ctx *context.APIContext, doer, blocker *user_model.User) { blockee, err := user_model.GetUserByName(ctx, ctx.PathParam("username")) if err != nil { - ctx.APIErrorNotFound("GetUserByName", err) + ctx.APIErrorAuto(err) return } if err := user_service.UnblockUser(ctx, doer, blocker, blockee); err != nil { if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/shared/runners.go b/routers/api/v1/shared/runners.go index 329f17736eb..fbb02627689 100644 --- a/routers/api/v1/shared/runners.go +++ b/routers/api/v1/shared/runners.go @@ -77,11 +77,7 @@ func getRunnerByID(ctx *context.APIContext, ownerID, repoID, runnerID int64) (*a runner, err := actions_model.GetRunnerByID(ctx, runnerID) if err != nil { - if errors.Is(err, util.ErrNotExist) { - ctx.APIErrorNotFound("Runner not found") - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return nil, false } diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index f5654908f0d..1086b221924 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -53,9 +53,9 @@ func CreateOrUpdateSecret(ctx *context.APIContext) { _, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Doer.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -95,9 +95,9 @@ func DeleteSecret(ctx *context.APIContext) { err := secret_service.DeleteSecretByName(ctx, ctx.Doer.ID, 0, ctx.PathParam("secretname")) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -148,13 +148,13 @@ func CreateVariable(ctx *context.APIContext) { return } if v != nil && v.ID > 0 { - ctx.APIError(http.StatusConflict, util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + ctx.APIError(http.StatusConflict, "variable name already exists") return } if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value, opt.Description); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -201,7 +201,7 @@ func UpdateVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -218,7 +218,7 @@ func UpdateVariable(ctx *context.APIContext) { if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -253,9 +253,9 @@ func DeleteVariable(ctx *context.APIContext) { if err := actions_service.DeleteVariableByName(ctx, ctx.Doer.ID, 0, ctx.PathParam("variablename")); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -292,7 +292,7 @@ func GetVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index 729c408a151..a410909e0e5 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -5,7 +5,6 @@ package user import ( - "errors" "fmt" "net/http" "strconv" @@ -112,13 +111,13 @@ func CreateAccessToken(ctx *context.APIContext) { return } if exist { - ctx.APIError(http.StatusBadRequest, errors.New("access token name has been used already")) + ctx.APIError(http.StatusBadRequest, "access token name has been used already") return } scope, err := auth_model.AccessTokenScope(strings.Join(form.Scopes, ",")).Normalize() if err != nil { - ctx.APIError(http.StatusBadRequest, fmt.Errorf("invalid access token scope provided: %w", err)) + ctx.APIError(http.StatusBadRequest, fmt.Sprintf("invalid access token scope provided: %v", err)) return } if scope == "" { @@ -188,7 +187,7 @@ func DeleteAccessToken(ctx *context.APIContext) { case 1: tokenID = tokens[0].ID default: - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("multiple matches for token name '%s'", token)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("multiple matches for token name '%s'", token)) return } } diff --git a/routers/api/v1/user/avatar.go b/routers/api/v1/user/avatar.go index d02ca87fb27..428ac0f62b7 100644 --- a/routers/api/v1/user/avatar.go +++ b/routers/api/v1/user/avatar.go @@ -32,7 +32,7 @@ func UpdateAvatar(ctx *context.APIContext) { content, err := base64.StdEncoding.DecodeString(form.Image) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } diff --git a/routers/api/v1/user/email.go b/routers/api/v1/user/email.go index 71b5a691c15..d3329f7a743 100644 --- a/routers/api/v1/user/email.go +++ b/routers/api/v1/user/email.go @@ -122,7 +122,7 @@ func DeleteEmail(ctx *context.APIContext) { if err := user_service.DeleteEmailAddresses(ctx, ctx.Doer, form.Emails); err != nil { if user_model.IsErrEmailAddressNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go index 11f659c85a4..47eabb5351d 100644 --- a/routers/api/v1/user/follower.go +++ b/routers/api/v1/user/follower.go @@ -233,7 +233,7 @@ func Follow(ctx *context.APIContext) { if err := user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go index bae4d7420f1..0148c7f5dac 100644 --- a/routers/api/v1/user/gpg_key.go +++ b/routers/api/v1/user/gpg_key.go @@ -4,7 +4,6 @@ package user import ( - "errors" "net/http" "strings" @@ -135,7 +134,7 @@ func GetGPGKey(ctx *context.APIContext) { // CreateUserGPGKey creates new GPG key to given user by ID. func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) { if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) { - ctx.APIErrorNotFound("Not Found", errors.New("gpg keys setting is not allowed to be visited")) + ctx.APIErrorNotFound("gpg keys setting is not allowed to be changed") return } @@ -276,7 +275,7 @@ func DeleteGPGKey(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) { - ctx.APIErrorNotFound("Not Found", errors.New("gpg keys setting is not allowed to be visited")) + ctx.APIErrorNotFound("gpg keys setting is not allowed to be changed") return } @@ -294,7 +293,7 @@ func HandleAddGPGKeyError(ctx *context.APIContext, err error, token string) { case asymkey_model.IsErrGPGKeyIDAlreadyUsed(err): ctx.APIError(http.StatusUnprocessableEntity, "A key with the same id already exists") case asymkey_model.IsErrGPGKeyParsing(err): - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) case asymkey_model.IsErrGPGNoEmailFound(err): ctx.APIError(http.StatusNotFound, "None of the emails attached to the GPG key could be found. It may still be added if you provide a valid signature for the token: "+token) case asymkey_model.IsErrGPGInvalidTokenSignature(err): diff --git a/routers/api/v1/user/helper.go b/routers/api/v1/user/helper.go index ce051e2d168..ee7b8b17275 100644 --- a/routers/api/v1/user/helper.go +++ b/routers/api/v1/user/helper.go @@ -18,7 +18,7 @@ func GetUserByPathParam(ctx *context.APIContext, name string) *user_model.User { if redirectUserID, err2 := user_model.LookupUserRedirect(ctx, username); err2 == nil { context.RedirectToUser(ctx.Base, ctx.Doer, username, redirectUserID) } else { - ctx.APIErrorNotFound("GetUserByName", err) + ctx.APIErrorNotFound() } } else { ctx.APIErrorInternal(err) diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go index 1a932c94af4..c12e98ca4c8 100644 --- a/routers/api/v1/user/key.go +++ b/routers/api/v1/user/key.go @@ -6,7 +6,6 @@ package user import ( std_ctx "context" - "errors" "net/http" asymkey_model "gitea.dev/models/asymkey" @@ -201,7 +200,7 @@ func GetPublicKey(ctx *context.APIContext) { // CreateUserPublicKey creates new public key to given user by ID. func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid int64) { if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { - ctx.APIErrorNotFound("Not Found", errors.New("ssh keys setting is not allowed to be visited")) + ctx.APIErrorNotFound("ssh keys setting is not allowed to be changed") return } @@ -271,7 +270,7 @@ func DeletePublicKey(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { - ctx.APIErrorNotFound("Not Found", errors.New("ssh keys setting is not allowed to be visited")) + ctx.APIErrorNotFound("ssh keys setting is not allowed to be changed") return } diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index 7036a4f854b..c78b6872c5a 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -174,7 +174,7 @@ func Star(ctx *context.APIContext) { err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) if err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index d45ca11348d..8343e380773 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -117,7 +117,7 @@ func GetInfo(ctx *context.APIContext) { if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) { // fake ErrUserNotExist error message to not leak information about existence - ctx.APIErrorNotFound("GetUserByName", user_model.ErrUserNotExist{Name: ctx.PathParam("username")}) + ctx.APIErrorNotFound() return } ctx.JSON(http.StatusOK, convert.ToUser(ctx, ctx.ContextUser, ctx.Doer)) diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go index 94d3de84b8a..2d1f55d8709 100644 --- a/routers/api/v1/user/watch.go +++ b/routers/api/v1/user/watch.go @@ -172,7 +172,7 @@ func Watch(ctx *context.APIContext) { err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) if err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/utils/sort.go b/routers/api/v1/utils/sort.go index b5f819cdb6e..669a09988ad 100644 --- a/routers/api/v1/utils/sort.go +++ b/routers/api/v1/utils/sort.go @@ -26,12 +26,12 @@ func ResolveSortOrder(ctx *context.APIContext, orderByMap map[string]map[string] } orderMap, ok := orderByMap[sortOrder] if !ok { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: %q", sortOrder)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort order: %q", sortOrder)) return "", false } orderBy, ok := orderMap[sortMode] if !ok { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: %q", sortMode)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort mode: %q", sortMode)) return "", false } return orderBy, true diff --git a/routers/common/actions.go b/routers/common/actions.go index 6fc5da7b13c..4c3c2129282 100644 --- a/routers/common/actions.go +++ b/routers/common/actions.go @@ -4,7 +4,9 @@ package common import ( + "errors" "fmt" + "io/fs" "strings" actions_model "gitea.dev/models/actions" @@ -51,6 +53,9 @@ func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename) if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return util.NewNotExistErrorf("logs not found") + } return fmt.Errorf("OpenLogs: %w", err) } defer reader.Close() diff --git a/routers/common/errpage.go b/routers/common/errpage.go index bb89070aaab..1426b05ef93 100644 --- a/routers/common/errpage.go +++ b/routers/common/errpage.go @@ -32,7 +32,7 @@ func renderServerErrorPage(w http.ResponseWriter, req *http.Request, respCode in } } - httpcache.SetCacheControlInHeader(w.Header(), &httpcache.CacheControlOptions{NoTransform: true}) + httpcache.SetCacheControlInHeader(w.Header(), &httpcache.CacheControlOptions{}) tmplCtx := context.NewTemplateContextForWeb(reqctx.FromContext(req.Context()), req, middleware.Locale(w, req)) w.WriteHeader(respCode) diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 7bed3523ed8..06c1b20d578 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -188,10 +188,6 @@ func SignInOAuthCallback(ctx *context.Context) { source := authSource.Cfg.(*oauth2.Source) - isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(source, &gothUser) - u.IsAdmin = isAdmin.ValueOrDefault(user_service.UpdateOptionField[bool]{FieldValue: false}).FieldValue - u.IsRestricted = isRestricted.ValueOrDefault(setting.Service.DefaultUserIsRestricted) - linkAccountData := &LinkAccountData{authSource.ID, gothUser} if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingDisabled { linkAccountData = nil @@ -368,14 +364,23 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m opts := &user_service.UpdateOptions{} - // Reactivate user if they are deactivated + // HINT: OAUTH-AUTO-SYNC-USER-ACTIVATION: see services/auth/source/oauth2/source_sync.go + // Reactivate user only if they were disabled by the OAuth2 auto sync cron (invalid_grant), + // which clears AccessToken/RefreshToken/ExpiresAt on the ExternalLoginUser row + // An admin-disabled user has no such signature, so we leave IsActive alone + // and let verifyAuthWithOptions route them through the prohibit-login / activate page. if !u.IsActive { - opts.IsActive = optional.Some(true) + extLogin, hasExt, err := user_model.GetExternalLogin(ctx, authSource.ID, gothUser.UserID) + if err != nil { + ctx.ServerError("GetExternalLogin", err) + return + } + isDisabledByAutoSync := hasExt && extLogin.RefreshToken == "" + if isDisabledByAutoSync { + opts.IsActive = optional.Some(true) + } } - // Update GroupClaims - opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser) - if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval { if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil { ctx.ServerError("SyncGroupsToTeams", err) diff --git a/routers/web/auth/oauth_signin_sync.go b/routers/web/auth/oauth_signin_sync.go index a939a0e71ec..f5ec66e006a 100644 --- a/routers/web/auth/oauth_signin_sync.go +++ b/routers/web/auth/oauth_signin_sync.go @@ -14,6 +14,7 @@ import ( asymkey_service "gitea.dev/services/asymkey" "gitea.dev/services/auth/source/oauth2" "gitea.dev/services/context" + user_service "gitea.dev/services/user" "github.com/markbates/goth" ) @@ -50,6 +51,14 @@ func oauth2SignInSync(ctx *context.Context, authSourceID int64, u *user_model.Us } } + // sync user flags (admin/restricted) + isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser) + if isAdmin.Has() || isRestricted.Has() { + if err = user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{IsAdmin: isAdmin, IsRestricted: isRestricted}); err != nil { + log.Error("Unable to sync OAuth2 user admin or restricted status %s: %v", gothUser.Provider, err) + } + } + err = oauth2UpdateSSHPubIfNeed(ctx, authSource, &gothUser, u) if err != nil { log.Error("Unable to sync OAuth2 SSH public key %s: %v", gothUser.Provider, err) diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 87c1ffdb2af..294bd7e1cb6 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -15,6 +15,7 @@ import ( "gitea.dev/models/asymkey" "gitea.dev/models/db" + "gitea.dev/models/gituser" user_model "gitea.dev/models/user" "gitea.dev/modules/badge" "gitea.dev/modules/charset" @@ -61,8 +62,8 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) { mockUser := mockUsers[0] commits = append(commits, &asymkey.SignCommit{ Verification: &asymkey.CommitVerification{}, - UserCommit: &user_model.UserCommit{ - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + UserCommit: &gituser.UserCommit{ + GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, }, }) commits = append(commits, &asymkey.SignCommit{ @@ -73,9 +74,9 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) { SigningKey: &asymkey.GPGKey{KeyID: "12345678"}, TrustStatus: "trusted", }, - UserCommit: &user_model.UserCommit{ - User: mockUser, - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + UserCommit: &gituser.UserCommit{ + AuthorUser: mockUser, + GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, }, }) commits = append(commits, &asymkey.SignCommit{ @@ -86,9 +87,9 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) { SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"}, TrustStatus: "untrusted", }, - UserCommit: &user_model.UserCommit{ - User: mockUser, - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + UserCommit: &gituser.UserCommit{ + AuthorUser: mockUser, + GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, }, }) commits = append(commits, &asymkey.SignCommit{ @@ -99,9 +100,9 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) { SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"}, TrustStatus: "other(unmatch)", }, - UserCommit: &user_model.UserCommit{ - User: mockUser, - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + UserCommit: &gituser.UserCommit{ + AuthorUser: mockUser, + GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, }, }) commits = append(commits, &asymkey.SignCommit{ @@ -110,9 +111,9 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) { Reason: "gpg.error", SigningEmail: "test@example.com", }, - UserCommit: &user_model.UserCommit{ - User: mockUser, - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + UserCommit: &gituser.UserCommit{ + AuthorUser: mockUser, + GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, }, }) @@ -159,6 +160,59 @@ func prepareMockDataBadgeActionsSvg(ctx *context.Context) { ctx.Data["SelectedStyle"] = selectedStyle } +func prepareMockDataAvatarStack(ctx *context.Context) { + /* + mockUsers, _ := db.Find[user_model.User](ctx, user_model.SearchUserOptions{ListOptions: db.ListOptions{PageSize: 3}}) + if len(mockUsers) == 0 { + return + } + u0 := mockUsers[0] + u1, u2 := u0, u0 + if len(mockUsers) >= 2 { + u1 = mockUsers[1] + } + if len(mockUsers) >= 3 { + u2 = mockUsers[2] + } + + authorSig := func(u *user_model.User) *git.Signature { + return &git.Signature{Name: u.Name, Email: u.Email} + } + coLinked := func(u *user_model.User) *gituser.CommitParticipant { + return &gituser.CommitParticipant{GiteaUser: u, GitIdentity: authorSig(u)} + } + coUnlinked := func(name, email string) *gituser.CommitParticipant { + return &gituser.CommitParticipant{GitIdentity: &git.Signature{Name: name, Email: email}} + } + nUnlinked := func(n int) []*gituser.CommitParticipant { + out := make([]*gituser.CommitParticipant, n) + for i := range out { + out[i] = coUnlinked(fmt.Sprintf("Contributor %d", i+1), fmt.Sprintf("contrib%d@example.com", i+1)) + } + return out + } + + type scenario struct { + Label string + Data *gituser.AvatarStackData + } + mk := gituser.BuildAvatarStackData() + extSig := &git.Signature{Name: "External Contributor", Email: "external@example.com"} + ctx.Data["AvatarStackScenarios"] = []scenario{ + {Label: "linked author, no co-authors", Data: mk(u0, authorSig(u0), nil)}, + {Label: "unlinked author, no co-authors", Data: mk(nil, extSig, nil)}, + {Label: "1 linked co-author", Data: mk(u0, authorSig(u0), []*gituser.CommitParticipant{coLinked(u1)})}, + {Label: "1 unlinked co-author", Data: mk(u0, authorSig(u0), []*gituser.CommitParticipant{coUnlinked("Bob Smith", "bob@example.com")})}, + {Label: "2 co-authors (3 people), u1 author", Data: mk(u1, authorSig(u1), []*gituser.CommitParticipant{coLinked(u0), coUnlinked("Bob Smith", "bob@example.com")})}, + {Label: "3 co-authors mixed (4 people)", Data: mk(u0, authorSig(u0), []*gituser.CommitParticipant{coLinked(u1), coLinked(u2), coUnlinked("Bob Smith", "bob@example.com")})}, + {Label: "9 co-authors (max visible, no overflow), u2 author", Data: mk(u2, authorSig(u2), nUnlinked(9))}, + {Label: "10 co-authors (overflow +1)", Data: mk(u0, authorSig(u0), nUnlinked(10))}, + {Label: "15 co-authors (overflow +6), unlinked author", Data: mk(nil, extSig, nUnlinked(15))}, + {Label: "30 co-authors (overflow +21)", Data: mk(u0, authorSig(u0), nUnlinked(30))}, + } + */ +} + func prepareMockDataRelativeTime(ctx *context.Context) { now := time.Now() ctx.Data["TimeNow"] = now @@ -196,6 +250,8 @@ func prepareMockData(ctx *context.Context) { prepareMockDataToastAndMessage(ctx) case "/devtest/unicode-escape": prepareMockDataUnicodeEscape(ctx) + case "/devtest/avatar-stack": + prepareMockDataAvatarStack(ctx) } } diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go index 69062ff6e78..bc6fdeb907f 100644 --- a/routers/web/devtest/mock_actions.go +++ b/routers/web/devtest/mock_actions.go @@ -15,6 +15,7 @@ import ( actions_model "gitea.dev/models/actions" user_model "gitea.dev/models/user" "gitea.dev/modules/setting" + "gitea.dev/modules/templates" "gitea.dev/modules/timeutil" "gitea.dev/modules/util" "gitea.dev/modules/web" @@ -87,29 +88,40 @@ func MockActionsRunsJobs(ctx *context.Context) { 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 - resp.State.Run.WorkflowID = "workflow-id" - resp.State.Run.WorkflowLink = "./workflow-link" + resp.State.Run.WorkflowID = "workflow-id.yml" resp.State.Run.TriggerEvent = "push" + renderUtils := templates.NewRenderUtils(ctx) + user2, _ := user_model.GetUserByID(ctx, 2) + if user2 == nil { + user2 = &user_model.User{Name: "user2"} + } + user3, _ := user_model.GetUserByID(ctx, 3) + if user3 == nil { + user3 = &user_model.User{Name: "user3"} + } resp.State.Run.Commit = actions.ViewCommit{ ShortSha: "ccccdddd", Link: "./commit-link", Pusher: actions.ViewUser{ - DisplayName: "pusher user", - Link: "./pusher-link", + DisplayName: user2.GetDisplayName(), + Link: user2.HomeLink(), + AvatarLink: user2.AvatarLinkWithSize(ctx, 16), }, Branch: actions.ViewBranch{ - Name: "commit-branch", + Name: "user2:commit-branch", Link: "./branch-link", IsDeleted: false, }, } + resp.State.Run.PullRequest = &actions.ViewPullRequest{ + Index: "#37658", + Link: "./pull/37658", + } now := time.Now() currentAttemptNum := int64(1) if attemptID > 0 { currentAttemptNum = attemptID } - user2 := &user_model.User{Name: "user2"} - user3 := &user_model.User{Name: "user3"} attempts := []*actions_model.ActionRunAttempt{{ Attempt: 1, Status: actions_model.StatusSuccess, @@ -168,15 +180,16 @@ func MockActionsRunsJobs(ctx *context.Context) { } } resp.State.Run.Attempts = append(resp.State.Run.Attempts, &actions.ViewRunAttempt{ - Attempt: attempt.Attempt, - Status: attempt.Status.String(), - Done: attempt.Status.IsDone(), - Link: link, - Current: current, - Latest: attempt.Attempt == latestAttempt.Attempt, - TriggeredAt: attempt.Created.AsTime().Unix(), - TriggerUserName: attempt.TriggerUser.GetDisplayName(), - TriggerUserLink: attempt.TriggerUser.HomeLink(), + Attempt: attempt.Attempt, + Status: attempt.Status.String(), + Done: attempt.Status.IsDone(), + Link: link, + Current: current, + Latest: attempt.Attempt == latestAttempt.Attempt, + TriggeredAt: attempt.Created.AsTime().Unix(), + TriggerUserName: attempt.TriggerUser.GetDisplayName(), + TriggerUserLink: attempt.TriggerUser.HomeLink(), + TriggerUserAvatar: attempt.TriggerUser.AvatarLinkWithSize(ctx, 16), }) } isLatestAttempt := currentAttemptNum == latestAttempt.Attempt @@ -185,6 +198,20 @@ func MockActionsRunsJobs(ctx *context.Context) { resp.State.Run.CanRerun = runID == 30 && isLatestAttempt 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"), + }, + } + resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{ Name: "artifact-a", Size: 100 * 1024, @@ -214,6 +241,75 @@ func MockActionsRunsJobs(ctx *context.Context) { return fmt.Sprintf("%s/jobs/%d", resp.State.Run.Link, jobID) } + // Keep devtest mock runs minimal: use run 10 as a "complex graph" repro. + // This combines long durations, parallel roots, and a multi-dependency downstream job + // to validate the workflow graph rendering. + if runID == 10 { + resp.State.Run.WorkflowID = "workflow-devtest-complex" + resp.State.Run.Duration = "7h 12m 34s" + + type mj struct { + jobID string + name string + status actions_model.Status + duration string + needs []string + } + mockJobs := []mj{ + {jobID: "job-100", name: "job-100", status: actions_model.StatusSuccess, duration: "3s", needs: nil}, + {jobID: "job-101", name: "job-101", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"job-100"}}, + {jobID: "job-102", name: "job-102", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"job-100", "job-101"}}, + {jobID: "job-103", name: "job-103", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"job-100"}}, + + {jobID: "prep-jdk", name: "prep-jdk", status: actions_model.StatusSuccess, duration: "3s", needs: nil}, + {jobID: "code-analysis", name: "code-analysis", status: actions_model.StatusSuccess, duration: "3s", needs: nil}, + + // Matrix expansion (the " (...)" suffix is the heuristic the frontend uses to group rows) + {jobID: "matrix-e2e-1-chromium", name: "matrix-e2e (1, chromium)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}}, + {jobID: "matrix-e2e-1-firefox", name: "matrix-e2e (1, firefox)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}}, + {jobID: "matrix-e2e-2-chromium", name: "matrix-e2e (2, chromium)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}}, + {jobID: "matrix-e2e-3-chromium", name: "matrix-e2e (3, chromium)", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"prep-jdk"}}, + {jobID: "matrix-e2e-3-firefox", name: "matrix-e2e (3, firefox)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}}, + {jobID: "matrix-e2e-99-webkit", name: "matrix-e2e (99, webkit)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}}, + + {jobID: "unit-test", name: "unit-test", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"prep-jdk"}}, + {jobID: "arch-test", name: "arch-test", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"prep-jdk"}}, + {jobID: "integration-test", name: "integration-test", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"prep-jdk"}}, + + {jobID: "build-image", name: "build-image", status: actions_model.StatusSuccess, duration: "3s", needs: []string{ + "unit-test", + "arch-test", + "integration-test", + "code-analysis", + "matrix-e2e-1-chromium", + "matrix-e2e-1-firefox", + "matrix-e2e-2-chromium", + "matrix-e2e-3-chromium", + "matrix-e2e-3-firefox", + "matrix-e2e-99-webkit", + }}, + } + + resp.State.Run.Jobs = nil + for i, j := range mockJobs { + id := runID*1000 + int64(i) + resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ + ID: id, + Link: jobLink(id), + JobID: j.jobID, + Name: j.name, + Status: j.status.String(), + CanRerun: j.jobID == "job-100", + Duration: j.duration, + Needs: j.needs, + }) + } + + fillViewRunResponseCurrentJob(ctx, resp) + ctx.JSON(http.StatusOK, resp) + return + } + resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ ID: runID * 10, Link: jobLink(runID * 10), @@ -240,7 +336,7 @@ func MockActionsRunsJobs(ctx *context.Context) { Name: "ULTRA LOOOOOOOOOOOONG job name 102 that exceeds the limit", Status: actions_model.StatusFailure.String(), CanRerun: false, - Duration: "3h", + Duration: "3h35m10s", Needs: []string{"job-100", "job-101"}, }) resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ diff --git a/routers/web/feed/file.go b/routers/web/feed/file.go index 122d219b6ca..f1155219286 100644 --- a/routers/web/feed/file.go +++ b/routers/web/feed/file.go @@ -20,7 +20,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( git.CommitsByFileAndRangeOptions{ Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName File: fileName, diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 5ae0d14a6fc..9c5e1664de0 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -27,6 +27,7 @@ import ( "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" "gitea.dev/services/convert" @@ -208,12 +209,20 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow if !hasJobWithoutNeeds && len(j.Needs()) == 0 { hasJobWithoutNeeds = true } + if j.Uses != "" { + if _, err := actions_service.ResolveUses(ctx, j.Uses); err != nil { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_reusable_workflow_uses", err.Error()) + break + } + } } - if !hasJobWithoutNeeds { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs") - } - if emptyJobsNumber == len(wf.Jobs) { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job") + if workflow.ErrMsg == "" { + if !hasJobWithoutNeeds { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs") + } + if emptyJobsNumber == len(wf.Jobs) { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job") + } } workflows = append(workflows, workflow) } @@ -352,7 +361,7 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo, otherWo return } for _, run := range runs { - if !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning) { + if !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning, actions_model.StatusBlocked) { continue } jobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(ctx, run.RepoID, run.ID) @@ -361,23 +370,31 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo, otherWo return } for _, job := range jobs { - if !job.Status.IsWaiting() { + 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()) break } - hasOnlineRunner := false - for _, runner := range runners { - if !runner.IsDisabled && runner.CanMatchLabels(job.RunsOn) { - hasOnlineRunner = true + 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()) break } } - if !hasOnlineRunner { - runErrors[run.ID] = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", strings.Join(job.RunsOn, ",")) - break + if job.Status.IsWaiting() { + hasOnlineRunner := false + for _, runner := range runners { + if !runner.IsDisabled && runner.CanMatchLabels(job.RunsOn) { + hasOnlineRunner = true + break + } + } + if !hasOnlineRunner { + runErrors[run.ID] = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", strings.Join(job.RunsOn, ",")) + break + } } } } diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index da997444490..2f4f1950ec5 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -20,14 +20,18 @@ import ( 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/models/unit" "gitea.dev/modules/actions" "gitea.dev/modules/base" + "gitea.dev/modules/cache" "gitea.dev/modules/git" "gitea.dev/modules/httplib" + "gitea.dev/modules/json" "gitea.dev/modules/log" "gitea.dev/modules/storage" + api "gitea.dev/modules/structs" "gitea.dev/modules/templates" "gitea.dev/modules/translation" "gitea.dev/modules/util" @@ -306,10 +310,13 @@ type ViewResponse struct { 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 TriggerEvent string `json:"triggerEvent"` // e.g. pull_request, push, schedule + + JobSummaries []*ViewJobSummary `json:"jobSummaries,omitempty"` } `json:"run"` CurrentJob struct { Title string `json:"title"` @@ -339,16 +346,28 @@ type ViewJob struct { CallUses string `json:"callUses,omitempty"` } +type ViewJobSummary struct { + JobID int64 `json:"jobId"` + JobName string `json:"jobName"` + SummaryHTML template.HTML `json:"summaryHTML"` +} + type ViewRunAttempt struct { - Attempt int64 `json:"attempt"` - Status string `json:"status"` - Done bool `json:"done"` - Link string `json:"link"` - Current bool `json:"current"` - Latest bool `json:"latest"` - TriggeredAt int64 `json:"triggeredAt"` - TriggerUserName string `json:"triggerUserName"` - TriggerUserLink string `json:"triggerUserLink"` + Attempt int64 `json:"attempt"` + Status string `json:"status"` + Done bool `json:"done"` + Link string `json:"link"` + Current bool `json:"current"` + Latest bool `json:"latest"` + TriggeredAt int64 `json:"triggeredAt"` + TriggerUserName string `json:"triggerUserName"` + TriggerUserLink string `json:"triggerUserLink"` + TriggerUserAvatar string `json:"triggerUserAvatar"` +} + +type ViewPullRequest struct { + Index string `json:"index"` + Link string `json:"link"` } type ViewCommit struct { @@ -361,6 +380,7 @@ type ViewCommit struct { type ViewUser struct { DisplayName string `json:"displayName"` Link string `json:"link"` + AvatarLink string `json:"avatarLink,omitempty"` } type ViewBranch struct { @@ -388,6 +408,132 @@ type ViewStepLogLine struct { Timestamp float64 `json:"timestamp"` } +func viewPullRequestFromRun(ctx context.Context, run *actions_model.ActionRun, prPayload *api.PullRequestPayload) *ViewPullRequest { + if run.Repo == nil { + return nil + } + refName := git.RefName(run.Ref) + if refName.IsPull() { + return &ViewPullRequest{ + Index: "#" + refName.ShortName(), + Link: run.RefLink(), + } + } + if prPayload != nil && prPayload.Index > 0 { + return &ViewPullRequest{ + Index: fmt.Sprintf("#%d", prPayload.Index), + Link: fmt.Sprintf("%s/pulls/%d", run.Repo.Link(), prPayload.Index), + } + } + // Push-triggered run: surface an open PR whose head matches this branch so + // users coming from a PR's check details can navigate back to it. + if refName.IsBranch() { + prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, run.RepoID, refName.ShortName()) + if err != nil { + log.Error("GetUnmergedPullRequestsByHeadInfo: %v", err) + } else if len(prs) == 1 { + pr := prs[0] + if err := pr.LoadBaseRepo(ctx); err != nil { + log.Error("LoadBaseRepo: %v", err) + return nil + } + return &ViewPullRequest{ + Index: fmt.Sprintf("#%d", pr.Index), + Link: fmt.Sprintf("%s/pulls/%d", pr.BaseRepo.Link(), pr.Index), + } + } + } + return nil +} + +func viewSummaryBranchFromRun(ctx context.Context, run *actions_model.ActionRun, prPayload *api.PullRequestPayload) ViewBranch { + refName := git.RefName(run.Ref) + if prPayload != nil && prPayload.PullRequest != nil && prPayload.PullRequest.Head != nil { + head := prPayload.PullRequest.Head + name := head.Name + if name == "" { + name = git.RefName(head.Ref).ShortName() + } + if head.Repository != nil && run.Repo != nil && head.RepoID > 0 && head.RepoID != run.Repo.ID { + ownerName := "" + if head.Repository.Owner != nil { + ownerName = head.Repository.Owner.UserName + } else if head.Repository.FullName != "" { + ownerName, _, _ = strings.Cut(head.Repository.FullName, "/") + } + if ownerName != "" && !strings.Contains(name, ":") { + name = ownerName + ":" + name + } + } + link := "" + if head.Repository != nil && head.Ref != "" { + repoLink := head.Repository.Link + if repoLink == "" { + repoLink = head.Repository.HTMLURL + } + if repoLink != "" { + link = repoLink + "/src/" + git.RefName(head.Ref).RefWebLinkPath() + } + } + return ViewBranch{Name: name, Link: link} + } + + branch := ViewBranch{ + Name: run.PrettyRef(), + Link: run.RefLink(), + } + if refName.IsBranch() { + b, err := git_model.GetBranch(ctx, run.RepoID, refName.ShortName()) + if err != nil && !git_model.IsErrBranchNotExist(err) { + log.Error("GetBranch: %v", err) + } else if git_model.IsErrBranchNotExist(err) || (b != nil && b.IsDeleted) { + branch.IsDeleted = true + } + } + return branch +} + +// actionsSummaryRefCacheTTL bounds how long the resolved PR/branch summary is +// cached. ViewPost is polled every second, but this metadata is stable for a +// run, so a short TTL collapses the repeated DB lookups while staying fresh +// enough for the navigation links. +const actionsSummaryRefCacheTTL = 10 // seconds + +type viewSummaryRefInfo struct { + PullRequest *ViewPullRequest `json:"pullRequest"` + Branch ViewBranch `json:"branch"` +} + +// getViewSummaryRefInfo resolves the run's pull request and head branch summary, +// caching the result briefly so the per-second poll does not hit the database on +// every request (GetUnmergedPullRequestsByHeadInfo / GetBranch). +func getViewSummaryRefInfo(ctx context.Context, run *actions_model.ActionRun) viewSummaryRefInfo { + compute := func() viewSummaryRefInfo { + // parse the event payload once and share it between both resolvers + prPayload, _ := run.GetPullRequestEventPayload() // nil unless this is a pull request event + return viewSummaryRefInfo{ + PullRequest: viewPullRequestFromRun(ctx, run, prPayload), + Branch: viewSummaryBranchFromRun(ctx, run, prPayload), + } + } + c := cache.GetCache() + if c == nil { + return compute() + } + cacheKey := fmt.Sprintf("actions_run_summary_ref:%d", run.ID) + if cached, ok := c.Get(cacheKey); ok && cached != "" { + var info viewSummaryRefInfo + if err := json.Unmarshal([]byte(cached), &info); err == nil { + return info + } + } + info := compute() + if data, err := json.Marshal(info); err == nil { + _ = c.Put(cacheKey, string(data), actionsSummaryRefCacheTTL) + } + return info +} + func ViewPost(ctx *context_module.Context) { run, attempt, jobs := getCurrentRunJobsByPathParam(ctx) if ctx.Written() { @@ -482,50 +628,71 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse, } for _, runAttempt := range attempts { resp.State.Run.Attempts = append(resp.State.Run.Attempts, &ViewRunAttempt{ - Attempt: runAttempt.Attempt, - Status: runAttempt.Status.String(), - Done: runAttempt.Status.IsDone(), - Link: getRunViewLink(run, runAttempt), - Current: runAttempt.ID == attempt.ID, - Latest: runAttempt.ID == run.LatestAttemptID, - TriggeredAt: runAttempt.Created.AsTime().Unix(), - TriggerUserName: runAttempt.TriggerUser.GetDisplayName(), - TriggerUserLink: runAttempt.TriggerUser.HomeLink(), + Attempt: runAttempt.Attempt, + Status: runAttempt.Status.String(), + Done: runAttempt.Status.IsDone(), + Link: getRunViewLink(run, runAttempt), + Current: runAttempt.ID == attempt.ID, + Latest: runAttempt.ID == run.LatestAttemptID, + TriggeredAt: runAttempt.Created.AsTime().Unix(), + TriggerUserName: runAttempt.TriggerUser.GetDisplayName(), + TriggerUserLink: runAttempt.TriggerUser.HomeLink(), + TriggerUserAvatar: runAttempt.TriggerUser.AvatarLinkWithSize(ctx, 16), }) } pusher := ViewUser{ DisplayName: run.TriggerUser.GetDisplayName(), Link: run.TriggerUser.HomeLink(), - } - branch := ViewBranch{ - Name: run.PrettyRef(), - Link: run.RefLink(), - } - refName := git.RefName(run.Ref) - if refName.IsBranch() { - b, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, refName.ShortName()) - if err != nil && !git_model.IsErrBranchNotExist(err) { - log.Error("GetBranch: %v", err) - } else if git_model.IsErrBranchNotExist(err) || (b != nil && b.IsDeleted) { - branch.IsDeleted = true - } + AvatarLink: run.TriggerUser.AvatarLinkWithSize(ctx, 16), } + refInfo := getViewSummaryRefInfo(ctx, run) resp.State.Run.Commit = ViewCommit{ ShortSha: base.ShortSha(run.CommitSHA), Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA), Pusher: pusher, - Branch: branch, + Branch: refInfo.Branch, } + resp.State.Run.PullRequest = refInfo.PullRequest resp.State.Run.TriggerEvent = run.TriggerEvent - // Legacy runs (LatestAttemptID == 0) have no attempt; their artifacts all share run_attempt_id=0, - // so passing 0 here scopes to this run's legacy artifacts only. + // Legacy runs (LatestAttemptID == 0) have no attempt; their artifacts and summaries all + // share run_attempt_id=0, so passing 0 here scopes to this run's legacy rows only. var runAttemptID int64 if attempt != nil { runAttemptID = attempt.ID } + + // Each step's markdown is rendered independently so an unclosed construct + // in one step can't bleed into the next. + // On a single-job view only that job's summaries are needed; the run view shows all. + // Scoping server-side avoids rendering every job's markdown on each 1s poll. + summaries, err := actions_model.ListActionRunJobSummaries(ctx, ctx.Repo.Repository.ID, run.ID, runAttemptID, ctx.PathParamInt64("job")) + if err != nil { + ctx.ServerError("ListActionRunJobSummaries", err) + return + } + if len(summaries) > 0 { + jobNameByID := make(map[int64]string, len(jobs)) + for _, j := range jobs { + jobNameByID[j.ID] = j.Name + } + renderUtils := templates.NewRenderUtils(ctx) + var current *ViewJobSummary + for _, s := range summaries { + if s.ContentType != actions_model.JobSummaryContentTypeMarkdown { + log.Warn("Skip unsupported job summary content type %q for run %d job %d step %d", s.ContentType, s.RunID, s.JobID, s.StepIndex) + continue + } + if current == nil || current.JobID != s.JobID { + current = &ViewJobSummary{JobID: s.JobID, JobName: jobNameByID[s.JobID]} + resp.State.Run.JobSummaries = append(resp.State.Run.JobSummaries, current) + } + current.SummaryHTML += renderUtils.MarkdownToHtml(s.Content) + } + } + arts, err := actions_model.ListUploadedArtifactsMetaByRunAttempt(ctx, ctx.Repo.Repository.ID, run.ID, runAttemptID) if err != nil { ctx.ServerError("ListUploadedArtifactsMetaByRunAttempt", err) diff --git a/routers/web/repo/actions/view_test.go b/routers/web/repo/actions/view_test.go index 737ac5e1b3a..020930eb38c 100644 --- a/routers/web/repo/actions/view_test.go +++ b/routers/web/repo/actions/view_test.go @@ -7,6 +7,8 @@ import ( "testing" actions_model "gitea.dev/models/actions" + repo_model "gitea.dev/models/repo" + api "gitea.dev/modules/structs" "gitea.dev/modules/timeutil" "gitea.dev/modules/translation" @@ -14,6 +16,66 @@ import ( "github.com/stretchr/testify/require" ) +func TestViewPullRequestFromRun(t *testing.T) { + repo := &repo_model.Repository{ID: 1, OwnerName: "owner", Name: "repo"} + + t.Run("pull ref", func(t *testing.T) { + run := &actions_model.ActionRun{Repo: repo, Ref: "refs/pull/123/head"} + assert.Equal(t, &ViewPullRequest{Index: "#123", Link: "/owner/repo/pulls/123"}, viewPullRequestFromRun(t.Context(), run, nil)) + }) + + t.Run("pull request event payload", func(t *testing.T) { + // a non-pull ref forces the payload branch instead of the ref branch + run := &actions_model.ActionRun{Repo: repo, Ref: "refs/heads/feature"} + payload := &api.PullRequestPayload{Index: 42} + assert.Equal(t, &ViewPullRequest{Index: "#42", Link: "/owner/repo/pulls/42"}, viewPullRequestFromRun(t.Context(), run, payload)) + }) + + t.Run("nil repo", func(t *testing.T) { + run := &actions_model.ActionRun{Ref: "refs/pull/1/head"} + assert.Nil(t, viewPullRequestFromRun(t.Context(), run, nil)) + }) +} + +func TestViewSummaryBranchFromRun(t *testing.T) { + repo := &repo_model.Repository{ID: 1, OwnerName: "owner", Name: "repo"} + + t.Run("pull request event same repo", func(t *testing.T) { + run := &actions_model.ActionRun{Repo: repo, Ref: "refs/pull/7/head"} + payload := &api.PullRequestPayload{ + PullRequest: &api.PullRequest{Head: &api.PRBranchInfo{ + Name: "feature", + Ref: "refs/heads/feature", + RepoID: 1, + Repository: &api.Repository{Link: "/owner/repo"}, + }}, + } + assert.Equal(t, ViewBranch{Name: "feature", Link: "/owner/repo/src/branch/feature"}, viewSummaryBranchFromRun(t.Context(), run, payload)) + }) + + t.Run("pull request event from fork prefixes owner", func(t *testing.T) { + run := &actions_model.ActionRun{Repo: repo, Ref: "refs/pull/7/head"} + payload := &api.PullRequestPayload{ + PullRequest: &api.PullRequest{Head: &api.PRBranchInfo{ + Name: "feature", + Ref: "refs/heads/feature", + RepoID: 2, + Repository: &api.Repository{ + Link: "/forkowner/repo", + Owner: &api.User{UserName: "forkowner"}, + }, + }}, + } + assert.Equal(t, ViewBranch{Name: "forkowner:feature", Link: "/forkowner/repo/src/branch/feature"}, viewSummaryBranchFromRun(t.Context(), run, payload)) + }) + + t.Run("push to tag does not query branch", func(t *testing.T) { + // a tag ref is not a branch, so no GetBranch DB lookup happens + run := &actions_model.ActionRun{Repo: repo, Ref: "refs/tags/v1.0.0"} + assert.Equal(t, ViewBranch{Name: "v1.0.0", Link: "/owner/repo/src/tag/v1.0.0"}, viewSummaryBranchFromRun(t.Context(), run, nil)) + }) +} + func TestConvertToViewModel(t *testing.T) { task := &actions_model.ActionTask{ Status: actions_model.StatusSuccess, diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index 6b97a58c17e..457f9795a23 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -12,8 +12,8 @@ import ( "path" "strconv" + "gitea.dev/models/gituser" repo_model "gitea.dev/models/repo" - user_model "gitea.dev/models/user" "gitea.dev/modules/charset" "gitea.dev/modules/git" "gitea.dev/modules/git/languagestats" @@ -29,13 +29,14 @@ import ( type blameRow struct { RowNumber int - Avatar template.HTML PreviousSha string PreviousShaURL string CommitURL string CommitMessage string CommitSince template.HTML + AvatarStackData *gituser.AvatarStackData + Code template.HTML EscapeStatus *charset.EscapeStatus } @@ -174,9 +175,9 @@ func fillBlameResult(br *gitrepo.BlameReader, r *blameResult) error { return nil } -func processBlameParts(ctx *context.Context, blameParts []*gitrepo.BlamePart) map[string]*user_model.UserCommit { +func processBlameParts(ctx *context.Context, blameParts []*gitrepo.BlamePart) map[string]*gituser.UserCommit { // store commit data by SHA to look up avatar info etc - commitNames := make(map[string]*user_model.UserCommit) + commitNames := make(map[string]*gituser.UserCommit) // and as blameParts can reference the same commits multiple // times, we cache the lookup work locally commits := make([]*git.Commit, 0, len(blameParts)) @@ -209,33 +210,28 @@ func processBlameParts(ctx *context.Context, blameParts []*gitrepo.BlamePart) ma } // populate commit email addresses to later look up avatars. - validatedCommits, err := user_model.ValidateCommitsWithEmails(ctx, commits) + userCommits, err := gituser.GetUserCommitsByGitCommits(ctx, commits, ctx.Repo.RepoLink, ctx.Repo.RefFullName) if err != nil { - ctx.ServerError("ValidateCommitsWithEmails", err) + ctx.ServerError("GetUserCommitsByGitCommits", err) return nil } - for _, c := range validatedCommits { - commitNames[c.ID.String()] = c + for _, c := range userCommits { + commitNames[c.GitCommit.ID.String()] = c } return commitNames } -func renderBlameFillFirstBlameRow(repoLink string, avatarUtils *templates.AvatarUtils, part *gitrepo.BlamePart, commit *user_model.UserCommit, br *blameRow) { - if commit.User != nil { - br.Avatar = avatarUtils.Avatar(commit.User, 18) - } else { - br.Avatar = avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18) - } - +func renderBlameFillFirstBlameRow(ctx *context.Context, repoLink string, part *gitrepo.BlamePart, commit *gituser.UserCommit, br *blameRow) { + br.AvatarStackData = gituser.BuildAvatarStackData(ctx, commit.GitCommit.AllParticipantIdentities(), nil) br.PreviousSha = part.PreviousSha br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(part.PreviousSha), util.PathEscapeSegments(part.PreviousPath)) br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha)) - br.CommitMessage = commit.MessageUTF8() - br.CommitSince = templates.TimeSince(commit.Author.When) + br.CommitMessage = commit.GitCommit.MessageUTF8() + br.CommitSince = templates.TimeSince(commit.GitCommit.Author.When) } -func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNames map[string]*user_model.UserCommit) { +func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNames map[string]*gituser.UserCommit) { language, err := languagestats.GetFileLanguage(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath) if err != nil { log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err) @@ -243,7 +239,6 @@ func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNa buf := &bytes.Buffer{} rows := make([]*blameRow, 0) - avatarUtils := templates.NewAvatarUtils(ctx) rowNumber := 0 // will be 1-based for _, part := range blameParts { for partLineIdx, line := range part.Lines { @@ -258,7 +253,7 @@ func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNa } if partLineIdx == 0 { - renderBlameFillFirstBlameRow(ctx.Repo.RepoLink, avatarUtils, part, commitNames[part.Sha], br) + renderBlameFillFirstBlameRow(ctx, ctx.Repo.RepoLink, part, commitNames[part.Sha], br) } } } diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 0760cb4eeda..d8953878184 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -14,6 +14,7 @@ import ( asymkey_model "gitea.dev/models/asymkey" "gitea.dev/models/db" git_model "gitea.dev/models/git" + "gitea.dev/models/gituser" issues_model "gitea.dev/models/issues" "gitea.dev/models/renderhelper" repo_model "gitea.dev/models/repo" @@ -49,7 +50,7 @@ func RefCommits(ctx *context.Context) { switch { case len(ctx.Repo.TreePath) == 0: Commits(ctx) - case ctx.Repo.TreePath == "search": + case ctx.Repo.TreePath == "search": // FIXME: legacy dirty design, it conflicts with the FileHistory SearchCommits(ctx) default: FileHistory(ctx) @@ -214,37 +215,52 @@ func FileHistory(ctx *context.Context) { return } - commitsCount, err := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository, ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath) - if err != nil { - ctx.ServerError("FileCommitsCount", err) - return - } else if commitsCount == 0 { - ctx.NotFound(nil) - return - } + followRename := ctx.FormBool("follow-rename") + ctx.Data["ShowFollowRename"] = true + ctx.Data["FollowRenameChecked"] = followRename page := max(ctx.FormInt("page"), 1) - - commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange( + commits, hasMore, err := ctx.Repo.GitRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ - Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName - File: ctx.Repo.TreePath, - Page: page, + Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName + File: ctx.Repo.TreePath, + Page: page, + FollowRename: followRename, }) if err != nil { ctx.ServerError("CommitsByFileAndRange", err) return } + + var commitsCount int64 + if followRename { + // there is no quick method to know the total count when "follow rename" + commitsCount = -1 + } else { + commitsCount, err = gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository, ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath) + if err != nil { + ctx.ServerError("FileCommitsCount", err) + return + } + } + + if len(commits) == 0 { + ctx.NotFound(nil) + return + } + + ctx.Data["FileTreePath"] = ctx.Repo.TreePath + ctx.Data["CommitCount"] = commitsCount ctx.Data["Commits"], err = processGitCommits(ctx, commits) if err != nil { ctx.ServerError("processGitCommits", err) return } - ctx.Data["FileTreePath"] = ctx.Repo.TreePath - ctx.Data["CommitCount"] = commitsCount - pager := context.NewPagination(commitsCount, setting.Git.CommitsRangeSize, page, 5) + if commitsCount == -1 { + pager.WithUnlimitedPaging(len(commits), hasMore) + } pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplCommits) @@ -381,7 +397,8 @@ func Diff(ctx *context.Context) { verification := asymkey_service.ParseCommitWithSignature(ctx, commit) ctx.Data["Verification"] = verification - ctx.Data["Author"] = user_model.ValidateCommitWithEmail(ctx, commit) + ctx.Data["Author"] = user_model.GetUserByGitAuthor(ctx, commit) + ctx.Data["CommitOtherParticipants"] = gituser.BuildAvatarStackData(ctx, commit.AllParticipantIdentities(), nil).Participants[1:] ctx.Data["Parents"] = parents ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0 @@ -396,14 +413,10 @@ func Diff(ctx *context.Context) { err = git.GetNote(ctx, ctx.Repo.GitRepo, commitID, note) if err == nil { ctx.Data["NoteCommit"] = note.Commit - ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit) + ctx.Data["NoteAuthor"] = user_model.GetUserByGitAuthor(ctx, note.Commit) rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefSubURL: "commit/" + util.PathEscapeSegments(commitID)}) htmlMessage := template.HTML(template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{})))) - ctx.Data["NoteRendered"], err = markup.PostProcessCommitMessage(rctx, htmlMessage) - if err != nil { - ctx.ServerError("PostProcessCommitMessage", err) - return - } + ctx.Data["NoteRendered"] = markup.PostProcessCommitMessage(rctx, htmlMessage) } else if !git.IsErrNotExist(err) { log.Error("GetNote: %v", err) } @@ -450,7 +463,7 @@ func RawDiff(ctx *context.Context) { } func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) ([]*git_model.SignCommitWithStatuses, error) { - commits, err := git_service.ConvertFromGitCommit(ctx, gitCommits, ctx.Repo.Repository) + commits, err := git_service.ConvertFromGitCommit(ctx, gitCommits, ctx.Repo.Repository, ctx.Repo.RefFullName) if err != nil { return nil, err } diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 2b2848afd15..45735fc8fe3 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -391,14 +391,14 @@ func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*g 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.MessageTitle() + title = c.UserCommit.GitCommit.MessageTitle() } else { title = autoTitleFromBranchName(ci.HeadRef.ShortName()) } if len(commits) == 1 { c := commits[0] - content = c.MessageBody() + content = c.GitCommit.MessageBody() } var titleTrailer string diff --git a/routers/web/repo/compare_test.go b/routers/web/repo/compare_test.go index d5b67ebe56e..af0a735227a 100644 --- a/routers/web/repo/compare_test.go +++ b/routers/web/repo/compare_test.go @@ -9,8 +9,8 @@ import ( asymkey_model "gitea.dev/models/asymkey" git_model "gitea.dev/models/git" + "gitea.dev/models/gituser" issues_model "gitea.dev/models/issues" - user_model "gitea.dev/models/user" "gitea.dev/modules/git" "gitea.dev/modules/setting" git_service "gitea.dev/services/git" @@ -52,8 +52,8 @@ func TestNewPullRequestTitleContent(t *testing.T) { mockCommit := func(msg string) *git_model.SignCommitWithStatuses { return &git_model.SignCommitWithStatuses{ SignCommit: &asymkey_model.SignCommit{ - UserCommit: &user_model.UserCommit{ - Commit: &git.Commit{ + UserCommit: &gituser.UserCommit{ + GitCommit: &git.Commit{ CommitMessage: git.CommitMessage{MessageRaw: msg}, }, }, diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 6862a3efc0f..eb6d5408a52 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -376,9 +376,7 @@ func (prInfo *pullRequestViewInfo) prepareViewFillCompareInfo(ctx *context.Conte pull := prInfo.issue.PullRequest prInfo.CompareInfo, err = git_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo, baseRef, git.RefName(pull.GetGitHeadRefName()), false, false) if err != nil { - isKnownErrorForBroken := gitcmd.IsStdErrorNotValidObjectName(err) || - // fatal: ambiguous argument 'origin': unknown revision or path not in the working tree. - gitcmd.StderrContains(err, "unknown revision or path not in the working tree") + isKnownErrorForBroken := gitcmd.IsStderr(err, gitcmd.StderrNotValidObjectName) || gitcmd.IsStderr(err, gitcmd.StderrUnknownRevisionOrPath) if !isKnownErrorForBroken { log.Error("GetCompareInfo: %v", err) } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 437eedc57f1..6df4e738c99 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -21,6 +21,7 @@ import ( 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" unit_model "gitea.dev/models/unit" user_model "gitea.dev/models/user" @@ -132,8 +133,11 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool { ctx.ServerError("CalculateTrustStatus", err) return false } + + avatarStackData := gituser.BuildAvatarStackData(ctx, latestCommit.AllParticipantIdentities(), nil) + avatarStackData.SearchByEmailLink = gituser.RepoCommitSearchByEmailLink(ctx.Repo.RepoLink, ctx.Repo.RefFullName) + ctx.Data["LatestCommitAvatarStackData"] = avatarStackData ctx.Data["LatestCommitVerification"] = verification - ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit) statuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll) if err != nil { diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 4d9cd21c132..6e2133a9452 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -351,7 +351,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( git.CommitsByFileAndRangeOptions{ Revision: ctx.Repo.Repository.DefaultWikiBranch, File: pageFilename, @@ -361,7 +361,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.ServerError("CommitsByFileAndRange", err) return nil, nil } - ctx.Data["Commits"], err = git_service.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository) + ctx.Data["Commits"], err = git_service.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository, "") // no current ref sub path for wiki commit list if err != nil { ctx.ServerError("ConvertFromGitCommit", err) return nil, nil @@ -496,7 +496,7 @@ func Wiki(ctx *context.Context) { ctx.ServerError("GetCommitByPath", err) return } - ctx.Data["Author"] = lastCommit.Author + ctx.Data["Committer"] = lastCommit.Committer ctx.HTML(http.StatusOK, tplWikiView) } @@ -528,7 +528,7 @@ func WikiRevision(ctx *context.Context) { ctx.ServerError("GetCommitByPath", err) return } - ctx.Data["Author"] = lastCommit.Author + ctx.Data["Committer"] = lastCommit.Committer ctx.HTML(http.StatusOK, tplWikiRevision) } @@ -587,7 +587,7 @@ func WikiPages(ctx *context.Context) { Name: displayName, SubURL: wiki_service.WebPathToURLPath(wikiName), GitEntryName: entry.Entry.Name(), - UpdatedUnix: timeutil.TimeStamp(entry.Commit.Author.When.Unix()), + UpdatedUnix: timeutil.TimeStamp(entry.Commit.Committer.When.Unix()), }) } ctx.Data["Pages"] = pages diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 3573a132414..6951be45ef3 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -111,6 +111,7 @@ func Dashboard(ctx *context.Context) { prepareHeatmapURL(ctx) + pageSize := setting.UI.User.RepoPagingNum feeds, count, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{ RequestedUser: ctxUser, RequestedTeam: ctx.Org.Team, @@ -119,17 +120,17 @@ func Dashboard(ctx *context.Context) { OnlyPerformedBy: false, IncludeDeleted: false, Date: ctx.FormString("date"), - ListOptions: db.ListOptions{ - Page: page, - PageSize: setting.UI.FeedPagingNum, - }, + ListOptions: db.ListOptions{Page: page, PageSize: pageSize}, }) if err != nil { ctx.ServerError("GetFeeds", err) return } - pager := context.NewPagination(count, setting.UI.FeedPagingNum, page, 5).WithCurRows(len(feeds)) + // FIXME: UNLIMITE-PAGING-ONE-MORE-ROW: here is still an edge case: when curRows==pagingNum, then the "next page" will be an empty page. + // Ideally we should query one more row to determine if there is really a next page, but it's impossible in current framework. + pager := context.NewPagination(count, pageSize, page, 5).WithUnlimitedPaging(len(feeds), len(feeds) == pageSize) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["Feeds"] = feeds diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index dfe9a12bc2f..654bd1a5f08 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -312,7 +312,8 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R pager := context.NewPagination(total, pagingNum, page, 5) if tab == "activity" { - pager.WithCurRows(curRows) + // FIXME: UNLIMITE-PAGING-ONE-MORE-ROW: see another comment + pager.WithUnlimitedPaging(curRows, curRows == pagingNum) } pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go index dc8f13cdcb2..7bf7c93dca8 100644 --- a/services/actions/cleanup.go +++ b/services/actions/cleanup.go @@ -232,6 +232,10 @@ func DeleteRun(ctx context.Context, run *actions_model.ActionRun) error { RepoID: repoID, RunID: run.ID, }) + recordsToDelete = append(recordsToDelete, &actions_model.ActionRunJobSummary{ + RepoID: repoID, + RunID: run.ID, + }) if err := db.WithTx(ctx, func(ctx context.Context) error { // TODO: Deleting task records could break current ephemeral runner implementation. This is a temporary workaround suggested by ChristopherHX. diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 616cf3d5c28..e67d1028f76 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -399,6 +399,24 @@ func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_mo } func ifNeedApproval(ctx context.Context, run *actions_model.ActionRun, repo *repo_model.Repository, user *user_model.User) (bool, error) { + canWrite := func(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (bool, error) { + perm, err := access_model.GetDoerRepoPermission(ctx, repo, user) + if err != nil { + return false, err + } + return perm.CanWrite(unit_model.TypeActions), nil + } + return ifNeedApprovalWith(ctx, run, repo, user, canWrite, issues_model.HasMergedPullRequestInRepo) +} + +func ifNeedApprovalWith( + ctx context.Context, + run *actions_model.ActionRun, + repo *repo_model.Repository, + user *user_model.User, + canWriteActions func(context.Context, *repo_model.Repository, *user_model.User) (bool, error), + hasMergedPR func(context.Context, int64, int64) (bool, error), +) (bool, error) { // 1. don't need approval if it's not a fork PR // 2. don't need approval if the event is `pull_request_target` since the workflow will run in the context of base branch // see https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks @@ -413,27 +431,24 @@ func ifNeedApproval(ctx context.Context, run *actions_model.ActionRun, repo *rep } // don't need approval if the user can write - if perm, err := access_model.GetDoerRepoPermission(ctx, repo, user); err != nil { + if ok, err := canWriteActions(ctx, repo, user); err != nil { return false, fmt.Errorf("GetDoerRepoPermission: %w", err) - } else if perm.CanWrite(unit_model.TypeActions) { + } else if ok { log.Trace("do not need approval because user %d can write", user.ID) return false, nil } - // don't need approval if the user has been approved before - if count, err := db.Count[actions_model.ActionRun](ctx, actions_model.FindRunOptions{ - RepoID: repo.ID, - TriggerUserID: user.ID, - Approved: true, - }); err != nil { - return false, fmt.Errorf("CountRuns: %w", err) - } else if count > 0 { - log.Trace("do not need approval because user %d has been approved before", user.ID) + // trust the user only after a merged PR — matching GitHub Actions. Approving one + // fork PR's run must not implicitly trust later fork PRs that replace the workflow. + if merged, err := hasMergedPR(ctx, repo.ID, user.ID); err != nil { + return false, fmt.Errorf("HasMergedPullRequestInRepo: %w", err) + } else if merged { + log.Trace("do not need approval because user %d has a merged pull request in repo %d", user.ID, repo.ID) return false, nil } // otherwise, need approval - log.Trace("need approval because it's the first time user %d triggered actions", user.ID) + log.Trace("need approval because user %d has no merged pull request in repo %d", user.ID, repo.ID) return true, nil } diff --git a/services/actions/notifier_helper_test.go b/services/actions/notifier_helper_test.go new file mode 100644 index 00000000000..3e33f344d9c --- /dev/null +++ b/services/actions/notifier_helper_test.go @@ -0,0 +1,102 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "errors" + "testing" + + actions_model "gitea.dev/models/actions" + repo_model "gitea.dev/models/repo" + user_model "gitea.dev/models/user" + actions_module "gitea.dev/modules/actions" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIfNeedApproval(t *testing.T) { + alwaysWrite := func(_ context.Context, _ *repo_model.Repository, _ *user_model.User) (bool, error) { + return true, nil + } + neverWrite := func(_ context.Context, _ *repo_model.Repository, _ *user_model.User) (bool, error) { + return false, nil + } + hasMerged := func(_ context.Context, _, _ int64) (bool, error) { return true, nil } + noMerged := func(_ context.Context, _, _ int64) (bool, error) { return false, nil } + errPerm := errors.New("perm error") + errMerge := errors.New("merge error") + + forkRun := &actions_model.ActionRun{IsForkPullRequest: true, TriggerEvent: actions_module.GithubEventPullRequest} + nonForkRun := &actions_model.ActionRun{IsForkPullRequest: false, TriggerEvent: actions_module.GithubEventPullRequest} + prTargetRun := &actions_model.ActionRun{IsForkPullRequest: true, TriggerEvent: actions_module.GithubEventPullRequestTarget} + + repo := &repo_model.Repository{ID: 1} + normalUser := &user_model.User{ID: 10} + restrictedUser := &user_model.User{ID: 11, IsRestricted: true} + + t.Run("not a fork PR never needs approval", func(t *testing.T) { + need, err := ifNeedApprovalWith(t.Context(), nonForkRun, repo, normalUser, alwaysWrite, hasMerged) + require.NoError(t, err) + assert.False(t, need) + }) + + t.Run("pull_request_target never needs approval even when fork", func(t *testing.T) { + need, err := ifNeedApprovalWith(t.Context(), prTargetRun, repo, normalUser, alwaysWrite, hasMerged) + require.NoError(t, err) + assert.False(t, need) + }) + + t.Run("restricted user always needs approval", func(t *testing.T) { + need, err := ifNeedApprovalWith(t.Context(), forkRun, repo, restrictedUser, alwaysWrite, hasMerged) + require.NoError(t, err) + assert.True(t, need) + }) + + t.Run("fork PR with write permission does not need approval", func(t *testing.T) { + need, err := ifNeedApprovalWith(t.Context(), forkRun, repo, normalUser, alwaysWrite, noMerged) + require.NoError(t, err) + assert.False(t, need) + }) + + t.Run("fork PR with merged PR but no write permission does not need approval", func(t *testing.T) { + need, err := ifNeedApprovalWith(t.Context(), forkRun, repo, normalUser, neverWrite, hasMerged) + require.NoError(t, err) + assert.False(t, need) + }) + + t.Run("fork PR with no write and no merged PR needs approval", func(t *testing.T) { + need, err := ifNeedApprovalWith(t.Context(), forkRun, repo, normalUser, neverWrite, noMerged) + require.NoError(t, err) + assert.True(t, need) + }) + + t.Run("canWriteActions error is propagated", func(t *testing.T) { + failWrite := func(_ context.Context, _ *repo_model.Repository, _ *user_model.User) (bool, error) { + return false, errPerm + } + _, err := ifNeedApprovalWith(t.Context(), forkRun, repo, normalUser, failWrite, noMerged) + require.ErrorIs(t, err, errPerm) + }) + + t.Run("hasMergedPR error is propagated", func(t *testing.T) { + failMerge := func(_ context.Context, _, _ int64) (bool, error) { return false, errMerge } + _, err := ifNeedApprovalWith(t.Context(), forkRun, repo, normalUser, neverWrite, failMerge) + require.ErrorIs(t, err, errMerge) + }) + + t.Run("restricted user skips permission check entirely", func(t *testing.T) { + // The perm and merge functions must not be called for a restricted user. + called := false + trackWrite := func(_ context.Context, _ *repo_model.Repository, _ *user_model.User) (bool, error) { + called = true + return true, nil + } + need, err := ifNeedApprovalWith(t.Context(), forkRun, repo, restrictedUser, trackWrite, noMerged) + require.NoError(t, err) + assert.True(t, need) + assert.False(t, called, "permission check must not run for restricted user") + }) +} diff --git a/services/actions/reusable_workflow.go b/services/actions/reusable_workflow.go index 9b5ecdef6f4..65a6acfbd03 100644 --- a/services/actions/reusable_workflow.go +++ b/services/actions/reusable_workflow.go @@ -6,6 +6,7 @@ package actions import ( "context" "fmt" + "strings" actions_model "gitea.dev/models/actions" "gitea.dev/models/db" @@ -15,7 +16,9 @@ import ( "gitea.dev/modules/actions/jobparser" "gitea.dev/modules/container" "gitea.dev/modules/gitrepo" + "gitea.dev/modules/httplib" "gitea.dev/modules/json" + "gitea.dev/modules/setting" api "gitea.dev/modules/structs" "gitea.dev/modules/util" "gitea.dev/services/convert" @@ -149,10 +152,10 @@ func expandReusableWorkflowCaller(ctx context.Context, run *actions_model.Action return fmt.Errorf("parse caller job %d: %w", caller.ID, err) } - // 3. Load called-workflow source. - ref, err := jobparser.ParseUses(parsedJob.Uses) + // 3. Resolve `uses` and load called-workflow source. + ref, err := ResolveUses(ctx, parsedJob.Uses) if err != nil { - return fmt.Errorf("parse uses %q: %w", parsedJob.Uses, err) + return fmt.Errorf("resolve uses %q: %w", parsedJob.Uses, err) } content, contentSourceRepoID, contentSourceCommitSHA, err := loadReusableWorkflowSource(ctx, run, caller, ref) if err != nil { @@ -340,3 +343,20 @@ func insertCallerChildren(ctx context.Context, run *actions_model.ActionRun, att } return nil } + +// ResolveUses normalizes and parses a reusable workflow `uses:` value. +// It first rewrites an absolute URL pointing to this instance into the cross-repo form (rejecting external URLs), +// then validates the syntax via jobparser.ParseUses. +func ResolveUses(ctx context.Context, uses string) (*jobparser.UsesRef, error) { + // Rewrite a local-instance URL to the equivalent cross-repo form "owner/repo/.gitea/workflows/file.yml@ref". + if strings.HasPrefix(uses, "http://") || strings.HasPrefix(uses, "https://") { + // ParseGiteaSiteURL returns nil for URLs that do not belong to this instance. + gsu := httplib.ParseGiteaSiteURL(ctx, uses) + if gsu == nil { + return nil, fmt.Errorf("unsupported reusable workflow URL %q: an absolute URL must point to this Gitea instance (%s)", uses, setting.AppURL) + } + // 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) +} diff --git a/services/actions/reusable_workflow_test.go b/services/actions/reusable_workflow_test.go index cadb26a851b..a7bb41ba8ae 100644 --- a/services/actions/reusable_workflow_test.go +++ b/services/actions/reusable_workflow_test.go @@ -10,6 +10,9 @@ import ( actions_model "gitea.dev/models/actions" "gitea.dev/models/db" "gitea.dev/models/unittest" + "gitea.dev/modules/actions/jobparser" + "gitea.dev/modules/setting" + "gitea.dev/modules/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -132,3 +135,44 @@ func buildCallerChain(t *testing.T, callerUses ...string) []*actions_model.Actio } return jobs } + +func TestResolveUses(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://gitea.example.com/sub/")() + defer test.MockVariableValue(&setting.AppSubURL, "/sub")() + ctx := t.Context() + + t.Run("LocalForms", func(t *testing.T) { + // Same-repo and cross-repo forms are not URLs and are parsed as-is. + ref, err := ResolveUses(ctx, "./.gitea/workflows/build.yml") + require.NoError(t, err) + assert.Equal(t, jobparser.UsesRef{Kind: jobparser.UsesKindLocalSameRepo, Path: ".gitea/workflows/build.yml"}, *ref) + + ref, err = ResolveUses(ctx, "owner/repo/.gitea/workflows/build.yml@v1") + require.NoError(t, err) + assert.Equal(t, jobparser.UsesRef{Kind: jobparser.UsesKindLocalCrossRepo, Owner: "owner", Repo: "repo", Path: ".gitea/workflows/build.yml", Ref: "v1"}, *ref) + }) + + 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") + require.NoError(t, err) + assert.Equal(t, jobparser.UsesRef{Kind: jobparser.UsesKindLocalCrossRepo, Owner: "owner", Repo: "repo", Path: ".gitea/workflows/ci.yml", Ref: "refs/heads/main"}, *ref) + }) + + t.Run("InvalidSyntax", func(t *testing.T) { + for _, in := range []string{ + "owner/.gitea/workflows/foo.yml", // missing repo segment + "owner/repo/.gitea/workflows/foo.yml", // missing @ref + "https://gitea.example.com/sub/repo/.gitea/workflows/ci.yml@refs/heads/main", // local absolute URL but missing owner + "not a valid uses at all", + } { + _, err := ResolveUses(ctx, in) + require.Error(t, err, "in = %s", in) + } + }) + + t.Run("ForeignURL", func(t *testing.T) { + _, err := ResolveUses(ctx, "https://other.gitea-example.com/owner/repo/.gitea/workflows/ci.yaml@v1") + assert.ErrorContains(t, err, "must point to this Gitea instance") + }) +} diff --git a/services/auth/source/oauth2/source_sync.go b/services/auth/source/oauth2/source_sync.go index 8fc58f9ae10..3123e54bbb2 100644 --- a/services/auth/source/oauth2/source_sync.go +++ b/services/auth/source/oauth2/source_sync.go @@ -88,8 +88,8 @@ func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *us } } - // Delete stored tokens, since they are invalid. This - // also provents us from checking this in subsequent runs. + // HINT: OAUTH-AUTO-SYNC-USER-ACTIVATION + // Delete stored tokens, since they are invalid. This also prevents us from checking this in subsequent runs. u.AccessToken = "" u.RefreshToken = "" u.ExpiresAt = time.Time{} diff --git a/services/context/api.go b/services/context/api.go index 97a97ecc550..02ec2b71384 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -137,16 +137,18 @@ func (ctx *APIContext) apiErrorInternal(skip int, err error) { }) } -// APIError responds with an error message to client with given obj as the message. -// If status is 500, also it prints error to log. -func (ctx *APIContext) APIError(status int, obj any) { - var message string - if err, ok := obj.(error); ok { - message = err.Error() - } else { - message = fmt.Sprintf("%s", obj) - } +// APIErrorNotFound handles 404s for APIContext +func (ctx *APIContext) APIErrorNotFound(msg ...string) { + ctx.JSON(http.StatusNotFound, APIError{ + Message: util.OptionalArg(msg, "not found"), + URL: setting.API.SwaggerURL, + }) +} +// APIError responds with an error message to client. +// If status is 500, also it prints error to log. +func (ctx *APIContext) APIError(status int, msg string) { + message := msg if status == http.StatusInternalServerError { log.ErrorWithSkip(1, "APIError: %s", message) @@ -161,6 +163,26 @@ func (ctx *APIContext) APIError(status int, obj any) { }) } +// APIErrorAuto use error check function to determine the response code +func (ctx *APIContext) APIErrorAuto(err error) { + switch { + case errors.Is(err, util.ErrInvalidArgument): + ctx.APIError(http.StatusBadRequest, err.Error()) + case errors.Is(err, util.ErrPermissionDenied): + ctx.APIError(http.StatusForbidden, err.Error()) + case errors.Is(err, util.ErrNotExist): + ctx.APIError(http.StatusNotFound, err.Error()) + case errors.Is(err, util.ErrAlreadyExist): + ctx.APIError(http.StatusConflict, err.Error()) + case errors.Is(err, util.ErrContentTooLarge): + ctx.APIError(http.StatusRequestEntityTooLarge, err.Error()) + case errors.Is(err, util.ErrUnprocessableContent): + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) + default: + ctx.apiErrorInternal(1, err) + } +} + type apiContextKeyType struct{} var apiContextKey = apiContextKeyType{} @@ -242,36 +264,12 @@ func APIContexter() func(http.Handler) http.Handler { } } - httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true}) + httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{}) next.ServeHTTP(ctx.Resp, ctx.Req) }) } } -// APIErrorNotFound handles 404s for APIContext -// String will replace message, errors will be added to a slice -func (ctx *APIContext) APIErrorNotFound(objs ...any) { - var message string - var errs []string - for _, obj := range objs { - // Ignore nil - if obj == nil { - continue - } - - if err, ok := obj.(error); ok { - errs = append(errs, err.Error()) - } else { - message = obj.(string) - } - } - ctx.JSON(http.StatusNotFound, map[string]any{ - "message": util.IfZero(message, "not found"), // do not use locale in API - "url": setting.API.SwaggerURL, - "errors": errs, - }) -} - // ReferencesGitRepo injects the GitRepo into the Context // you can optional skip the IsEmpty check func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) { @@ -329,17 +327,6 @@ func RepoRefForAPI(next http.Handler) http.Handler { }) } -// NotFoundOrServerError use error check function to determine if the error -// is about not found. It responds with 404 status code for not found error, -// or error context description for logging purpose of 500 server error. -func (ctx *APIContext) NotFoundOrServerError(err error) { - if errors.Is(err, util.ErrNotExist) { - ctx.JSON(http.StatusNotFound, nil) - return - } - ctx.APIErrorInternal(err) -} - // IsUserSiteAdmin returns true if current user is a site admin func (ctx *APIContext) IsUserSiteAdmin() bool { return ctx.IsSigned && ctx.Doer.IsAdmin diff --git a/services/context/context.go b/services/context/context.go index 2875bb46161..c1e438975f6 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -196,7 +196,7 @@ func Contexter() func(next http.Handler) http.Handler { } } - httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true}) + httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{}) ctx.Data["SystemConfig"] = setting.Config() diff --git a/services/context/package.go b/services/context/package.go index 4918e124913..7c1e55c8a34 100644 --- a/services/context/package.go +++ b/services/context/package.go @@ -34,11 +34,8 @@ type packageAssignmentCtx struct { // PackageAssignment returns a middleware to handle Context.Package assignment func PackageAssignment() func(ctx *Context) { return func(ctx *Context) { - errorFn := func(status int, obj any) { - err, ok := obj.(error) - if !ok { - err = fmt.Errorf("%s", obj) - } + errorFn := func(status int, msg string) { + err := fmt.Errorf("%s", msg) if status == http.StatusNotFound { ctx.NotFound(err) } else { @@ -58,11 +55,11 @@ func PackageAssignmentAPI() func(ctx *APIContext) { } } -func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, any)) *Package { +func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string)) *Package { pkgOwner := ctx.ContextUser accessMode, err := determineAccessMode(ctx.Base, pkgOwner, ctx.Doer) if err != nil { - errCb(http.StatusInternalServerError, fmt.Errorf("determineAccessMode: %w", err)) + errCb(http.StatusInternalServerError, fmt.Sprintf("determineAccessMode: %v", err)) return nil } @@ -81,25 +78,25 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, any)) *Package pv, err := packages_model.GetVersionByNameAndVersion(ctx, pkg.Owner.ID, packages_model.Type(packageType), name, version) if err != nil { if errors.Is(err, packages_model.ErrPackageNotExist) { - errCb(http.StatusNotFound, fmt.Errorf("GetVersionByNameAndVersion: %w", err)) + errCb(http.StatusNotFound, fmt.Sprintf("GetVersionByNameAndVersion: %v", err)) } else { - errCb(http.StatusInternalServerError, fmt.Errorf("GetVersionByNameAndVersion: %w", err)) + errCb(http.StatusInternalServerError, fmt.Sprintf("GetVersionByNameAndVersion: %v", err)) } return pkg } pkg.Descriptor, err = packages_model.GetPackageDescriptor(ctx, pv) if err != nil { - errCb(http.StatusInternalServerError, fmt.Errorf("GetPackageDescriptor: %w", err)) + errCb(http.StatusInternalServerError, fmt.Sprintf("GetPackageDescriptor: %v", err)) return pkg } } else { p, err := packages_model.GetPackageByName(ctx, pkg.Owner.ID, packages_model.Type(packageType), name) if err != nil { if errors.Is(err, packages_model.ErrPackageNotExist) { - errCb(http.StatusNotFound, fmt.Errorf("GetPackageByName: %w", err)) + errCb(http.StatusNotFound, fmt.Sprintf("GetPackageByName: %v", err)) } else { - errCb(http.StatusInternalServerError, fmt.Errorf("GetPackageByName: %w", err)) + errCb(http.StatusInternalServerError, fmt.Sprintf("GetPackageByName: %v", err)) } return pkg } diff --git a/services/context/pagination.go b/services/context/pagination.go index a6e83df6a15..ef0f44ab37c 100644 --- a/services/context/pagination.go +++ b/services/context/pagination.go @@ -33,8 +33,8 @@ func NewPagination(total int64, pagingNum, current, numPages int) *Pagination { return p } -func (p *Pagination) WithCurRows(n int) *Pagination { - p.Paginater.SetCurRows(n) +func (p *Pagination) WithUnlimitedPaging(curRows int, hasNext bool) *Pagination { + p.Paginater.SetUnlimitedPaging(curRows, hasNext) return p } diff --git a/services/context/pagination_test.go b/services/context/pagination_test.go index 0ddef26ca3d..1b15fa7b81d 100644 --- a/services/context/pagination_test.go +++ b/services/context/pagination_test.go @@ -32,4 +32,24 @@ func TestPagination(t *testing.T) { params.Del("foo") v, _ = url.ParseQuery(string(p.GetParams())) assert.Equal(t, params, v) + + p = NewPagination(-1, 1, 1, 1) + p.WithUnlimitedPaging(0, false) + assert.Zero(t, p.Paginater.TotalPages()) + assert.False(t, p.Paginater.HasNext()) + + p = NewPagination(-1, 1, 1, 1) + p.WithUnlimitedPaging(10, false) + assert.Equal(t, 1, p.Paginater.TotalPages()) // first page, no next, so it should know that the total page number is 1 + assert.False(t, p.Paginater.HasNext()) + + p = NewPagination(-1, 1, 2, 1) + p.WithUnlimitedPaging(10, false) + assert.Equal(t, -1, p.Paginater.TotalPages()) + assert.False(t, p.Paginater.HasNext()) + + p = NewPagination(-1, 1, 1, 1) + p.WithUnlimitedPaging(10, true) + assert.Equal(t, -1, p.Paginater.TotalPages()) + assert.True(t, p.Paginater.HasNext()) } diff --git a/services/context/user.go b/services/context/user.go index ad0876ebf00..d335b9738a5 100644 --- a/services/context/user.go +++ b/services/context/user.go @@ -14,11 +14,8 @@ import ( // UserAssignmentWeb returns a middleware to handle context-user assignment for web routes func UserAssignmentWeb() func(ctx *Context) { return func(ctx *Context) { - errorFn := func(status int, obj any) { - err, ok := obj.(error) - if !ok { - err = fmt.Errorf("%s", obj) - } + errorFn := func(status int, msg string) { + err := fmt.Errorf("%s", msg) if status == http.StatusNotFound { ctx.NotFound(err) } else { @@ -37,7 +34,7 @@ func UserAssignmentAPI() func(ctx *APIContext) { } } -func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, any)) (contextUser *user_model.User) { +func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, string)) (contextUser *user_model.User) { username := ctx.PathParam("username") if doer != nil && strings.EqualFold(doer.LowerName, username) { @@ -50,12 +47,12 @@ func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, any)) (con if redirectUserID, err := user_model.LookupUserRedirect(ctx, username); err == nil { RedirectToUser(ctx, doer, username, redirectUserID) } else if user_model.IsErrUserRedirectNotExist(err) { - errCb(http.StatusNotFound, err) + errCb(http.StatusNotFound, err.Error()) } else { - errCb(http.StatusInternalServerError, fmt.Errorf("LookupUserRedirect: %w", err)) + errCb(http.StatusInternalServerError, fmt.Sprintf("LookupUserRedirect: %v", err)) } } else { - errCb(http.StatusInternalServerError, fmt.Errorf("GetUserByName: %w", err)) + errCb(http.StatusInternalServerError, fmt.Sprintf("GetUserByName: %v", err)) } } } diff --git a/services/git/commit.go b/services/git/commit.go index 5708e162b99..bc0516b5d6f 100644 --- a/services/git/commit.go +++ b/services/git/commit.go @@ -9,6 +9,7 @@ import ( 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" user_model "gitea.dev/models/user" "gitea.dev/modules/container" @@ -17,14 +18,14 @@ import ( ) // ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys. -func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository, oldCommits []*user_model.UserCommit, repoTrustModel repo_model.TrustModelType) ([]*asymkey_model.SignCommit, error) { +func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository, oldCommits []*gituser.UserCommit, repoTrustModel repo_model.TrustModelType) ([]*asymkey_model.SignCommit, error) { newCommits := make([]*asymkey_model.SignCommit, 0, len(oldCommits)) keyMap := map[string]bool{} emails := make(container.Set[string]) for _, c := range oldCommits { - if c.Committer != nil { - emails.Add(c.Committer.Email) + if c.GitCommit.Committer != nil { + emails.Add(c.GitCommit.Committer.Email) } } @@ -34,10 +35,10 @@ func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository, } for _, c := range oldCommits { - committerUser := emailUsers.GetByEmail(c.Committer.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"? + committerUser := emailUsers.GetByEmail(c.GitCommit.Committer.Email) // FIXME: why GetUserCommitsByGitCommits uses "Author", but ParseCommitsWithSignature uses "Committer"? signCommit := &asymkey_model.SignCommit{ UserCommit: c, - Verification: asymkey_service.ParseCommitWithSignatureCommitter(ctx, c.Commit, committerUser), + Verification: asymkey_service.ParseCommitWithSignatureCommitter(ctx, c.GitCommit, committerUser), } isOwnerMemberCollaborator := func(user *user_model.User) (bool, error) { @@ -52,15 +53,15 @@ func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository, } // ConvertFromGitCommit converts git commits into SignCommitWithStatuses -func ConvertFromGitCommit(ctx context.Context, commits []*git.Commit, repo *repo_model.Repository) ([]*git_model.SignCommitWithStatuses, error) { - validatedCommits, err := user_model.ValidateCommitsWithEmails(ctx, commits) +func ConvertFromGitCommit(ctx context.Context, commits []*git.Commit, repo *repo_model.Repository, currentRef git.RefName) ([]*git_model.SignCommitWithStatuses, error) { + userCommits, err := gituser.GetUserCommitsByGitCommits(ctx, commits, repo.Link(), currentRef) if err != nil { return nil, err } signedCommits, err := ParseCommitsWithSignature( ctx, repo, - validatedCommits, + userCommits, repo.GetTrustModel(), ) if err != nil { @@ -77,7 +78,7 @@ func ParseCommitsWithStatus(ctx context.Context, oldCommits []*asymkey_model.Sig commit := &git_model.SignCommitWithStatuses{ SignCommit: c, } - statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commit.ID.String(), db.ListOptionsAll) + statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commit.GitCommit.ID.String(), db.ListOptionsAll) if err != nil { return nil, err } diff --git a/services/issue/comments.go b/services/issue/comments.go index 46964f5f1a3..ff6c9fccf56 100644 --- a/services/issue/comments.go +++ b/services/issue/comments.go @@ -184,7 +184,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) + c.Commits, err = git_service.ConvertFromGitCommit(ctx, gitRepo.GetCommitsFromIDs(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 055c1023b4b..260ac5ceaec 100644 --- a/services/issue/pull.go +++ b/services/issue/pull.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "slices" + "time" issues_model "gitea.dev/models/issues" org_model "gitea.dev/models/organization" @@ -26,6 +27,10 @@ type ReviewRequestNotifier struct { var codeOwnerFiles = []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} +// codeOwnerMatchBudget caps the total wall-clock time spent evaluating all +// CODEOWNERS rules against all changed files for a single PR. +const codeOwnerMatchBudget = 2 * time.Second + func IsCodeOwnerFile(f string) bool { return slices.Contains(codeOwnerFiles, f) } @@ -93,8 +98,17 @@ func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullReque uniqUsers := make(map[int64]*user_model.User) uniqTeams := make(map[string]*org_model.Team) + // Bound the total time spent matching rules×files. The per-rule MatchTimeout + // only caps a single match; without an aggregate budget a crafted CODEOWNERS + // plus a PR touching many files could still exhaust CPU inside this loop. + matchDeadline := time.Now().Add(codeOwnerMatchBudget) +ruleLoop: for _, rule := range rules { for _, f := range changedFiles { + if time.Now().After(matchDeadline) { + log.Warn("CODEOWNERS matching for PR %s#%d exceeded its time budget; some rules were not evaluated", pr.BaseRepo.FullName(), pr.ID) + break ruleLoop + } shouldMatch := !rule.Negative matched, _ := rule.Rule.MatchString(f) // err only happens when timeouts, any error can be considered as not matched if matched == shouldMatch { diff --git a/services/migrations/error.go b/services/migrations/error.go index d9a6b8f2675..2dd5540f0ec 100644 --- a/services/migrations/error.go +++ b/services/migrations/error.go @@ -7,7 +7,7 @@ package migrations import ( "errors" - "github.com/google/go-github/v87/github" + "github.com/google/go-github/v88/github" ) // ErrRepoNotCreated returns the error that repository not created diff --git a/services/migrations/github.go b/services/migrations/github.go index e31a2fa6057..b8bf6aee453 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/v87/github" + "github.com/google/go-github/v88/github" "golang.org/x/oauth2" ) diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go index 67ec5bb81cf..229730257f2 100644 --- a/services/pull/merge_squash.go +++ b/services/pull/merge_squash.go @@ -65,9 +65,7 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error { } if setting.Repository.PullRequest.AddCoCommitterTrailers && ctx.committer.String() != sig.String() { - // add trailer - message = AddCommitMessageTailer(message, "Co-authored-by", sig.String()) - message = AddCommitMessageTailer(message, "Co-committed-by", sig.String()) // FIXME: this one should be removed, it is not really used or widely used + message = AddCommitMessageTailer(message, git.CoAuthoredByTrailer, sig.String()) } cmdCommit := gitcmd.NewCommand("commit"). AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email). diff --git a/services/pull/pull.go b/services/pull/pull.go index 0d4a10ae9ef..a601450ff25 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -917,7 +917,7 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ } for _, author := range authors { - stringBuilder.WriteString("Co-authored-by: ") + stringBuilder.WriteString(git.CoAuthoredByTrailer + ": ") stringBuilder.WriteString(author) stringBuilder.WriteRune('\n') } diff --git a/services/release/notes.go b/services/release/notes.go index b8baeb9620a..e62335c98b5 100644 --- a/services/release/notes.go +++ b/services/release/notes.go @@ -10,6 +10,7 @@ import ( "slices" "strings" + "gitea.dev/models/db" issues_model "gitea.dev/models/issues" repo_model "gitea.dev/models/repo" user_model "gitea.dev/models/user" @@ -32,18 +33,23 @@ func GenerateReleaseNotes(ctx context.Context, repo *repo_model.Repository, gitR return "", err } - if opts.PreviousTag == "" { - // no previous tag, usually due to there is no tag in the repo, use the same content as GitHub - content := fmt.Sprintf("**Full Changelog**: %s/commits/tag/%s\n", repo.HTMLURL(ctx), util.PathEscapeSegments(opts.TagName)) - return content, nil + isFirstRelease, err := isFirstRelease(ctx, repo.ID) + if err != nil { + return "", fmt.Errorf("isFirstRelease: %w", err) } - baseCommit, err := gitRepo.GetCommit(opts.PreviousTag) - if err != nil { + baseCommitID := "" + if opts.PreviousTag != "" { + baseCommit, err := gitRepo.GetCommit(opts.PreviousTag) + if err != nil { + return "", util.ErrorWrapTranslatable(util.ErrNotExist, "repo.release.generate_notes_tag_not_found", opts.TagName) + } + baseCommitID = baseCommit.ID.String() + } else if !isFirstRelease { return "", util.ErrorWrapTranslatable(util.ErrNotExist, "repo.release.generate_notes_tag_not_found", opts.TagName) } - commits, err := gitRepo.CommitsBetweenIDs(headCommit.ID.String(), baseCommit.ID.String()) + commits, err := gitRepo.CommitsBetweenIDs(headCommit.ID.String(), baseCommitID) if err != nil { return "", fmt.Errorf("CommitsBetweenIDs: %w", err) } @@ -58,10 +64,27 @@ func GenerateReleaseNotes(ctx context.Context, repo *repo_model.Repository, gitR return "", err } - content := buildReleaseNotesContent(ctx, repo, opts.TagName, opts.PreviousTag, prs, contributors, newContributors) + fullChangelogURL := "" + if isFirstRelease { + // Keep the first-release changelog link aligned with GitHub, while collecting PRs from full history. + fullChangelogURL = fmt.Sprintf("%s/commits/tag/%s", repo.HTMLURL(ctx), util.PathEscapeSegments(opts.TagName)) + } + + content := buildReleaseNotesContent(ctx, repo, opts.TagName, opts.PreviousTag, prs, contributors, newContributors, fullChangelogURL) return content, nil } +func isFirstRelease(ctx context.Context, repoID int64) (bool, error) { + count, err := db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{ + RepoID: repoID, + IncludeDrafts: false, + }) + if err != nil { + return false, err + } + return count == 0, nil +} + func resolveHeadCommit(gitRepo *git.Repository, tagName, tagTarget string) (*git.Commit, error) { ref := tagName if !gitRepo.IsTagExist(tagName) { @@ -107,7 +130,7 @@ func collectPullRequestsFromCommits(ctx context.Context, repoID int64, commits [ return prs, nil } -func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, tagName, baseRef string, prs []*issues_model.PullRequest, contributors []*user_model.User, newContributors []*issues_model.PullRequest) string { +func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, tagName, baseRef string, prs []*issues_model.PullRequest, contributors []*user_model.User, newContributors []*issues_model.PullRequest, fullChangelogURL string) string { var builder strings.Builder builder.WriteString("## What's Changed\n") @@ -136,8 +159,12 @@ func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, } builder.WriteString("**Full Changelog**: ") - compareURL := fmt.Sprintf("%s/compare/%s...%s", repo.HTMLURL(ctx), util.PathEscapeSegments(baseRef), util.PathEscapeSegments(tagName)) - fmt.Fprintf(&builder, "[%s...%s](%s)", baseRef, tagName, compareURL) + if fullChangelogURL != "" { + builder.WriteString(fullChangelogURL) + } else { + compareURL := fmt.Sprintf("%s/compare/%s...%s", repo.HTMLURL(ctx), util.PathEscapeSegments(baseRef), util.PathEscapeSegments(tagName)) + fmt.Fprintf(&builder, "[%s...%s](%s)", baseRef, tagName, compareURL) + } builder.WriteByte('\n') return builder.String() } diff --git a/services/release/notes_test.go b/services/release/notes_test.go index 2922da424b5..3fb3b7c553a 100644 --- a/services/release/notes_test.go +++ b/services/release/notes_test.go @@ -21,13 +21,14 @@ import ( func TestGenerateReleaseNotes(t *testing.T) { unittest.PrepareTestEnv(t) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - gitRepo, err := gitrepo.OpenRepository(t.Context(), repo) - require.NoError(t, err) - t.Run("ChangeLogsWithPRs", func(t *testing.T) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + gitRepo, err := gitrepo.OpenRepository(t.Context(), repo) + require.NoError(t, err) + t.Cleanup(func() { gitRepo.Close() }) + mergedCommit := "90c1019714259b24fb81711d4416ac0f18667dfa" - createMergedPullRequest(t, repo, mergedCommit, 5) + createMergedPullRequest(t, repo, mergedCommit, 5, "Release notes test pull request") content, err := GenerateReleaseNotes(t.Context(), repo, gitRepo, GenerateReleaseNotesOptions{ TagName: "v1.2.0", @@ -50,16 +51,51 @@ 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) + require.NoError(t, err) + t.Cleanup(func() { gitRepo.Close() }) + + createMergedPullRequest(t, repo, "69554a64c1e6030f051e5c3f94bfbd773cd6a324", 5, "Initial tag PR 1") + createMergedPullRequest(t, repo, "27566bd5738fc8b4e3fef3c5e72cce608537bd95", 4, "Initial tag PR 2") + createMergedPullRequest(t, repo, "5099b81332712fe655e34e8dd63574f503f61811", 8, "Initial tag PR 3") + content, err := GenerateReleaseNotes(t.Context(), repo, gitRepo, GenerateReleaseNotesOptions{ - TagName: "v1.2.0", - TagTarget: "DefaultBranch", + TagName: "v0.1.0", + TagTarget: repo.DefaultBranch, }) require.NoError(t, err) - assert.Equal(t, "**Full Changelog**: https://try.gitea.io/user2/repo1/commits/tag/v1.2.0\n", content) + + assert.Contains(t, content, "## What's Changed\n") + assert.Contains(t, content, "* Initial tag PR 1 in [#") + assert.Contains(t, content, "* Initial tag PR 2 in [#") + assert.Contains(t, content, "* Initial tag PR 3 in [#") + assert.Contains(t, content, "\n## Contributors\n") + assert.Contains(t, content, "* @user5\n") + assert.Contains(t, content, "* @user4\n") + assert.Contains(t, content, "* @user8\n") + assert.Contains(t, content, "\n## New Contributors\n") + assert.Contains(t, content, "* @user5 made their first contribution in [#") + assert.Contains(t, content, "* @user4 made their first contribution in [#") + assert.Contains(t, content, "* @user8 made their first contribution in [#") + assert.Contains(t, content, "**Full Changelog**: https://try.gitea.io/user2/repo16/commits/tag/v0.1.0\n") + }) + + t.Run("EmptyPreviousTagWithExistingTags", func(t *testing.T) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + gitRepo, err := gitrepo.OpenRepository(t.Context(), repo) + require.NoError(t, err) + t.Cleanup(func() { gitRepo.Close() }) + + _, err = GenerateReleaseNotes(t.Context(), repo, gitRepo, GenerateReleaseNotesOptions{ + TagName: "v1.2.0", + TagTarget: repo.DefaultBranch, + }) + require.Error(t, err) }) } -func createMergedPullRequest(t *testing.T, repo *repo_model.Repository, mergeCommit string, posterID int64) *issues_model.PullRequest { +func createMergedPullRequest(t *testing.T, repo *repo_model.Repository, mergeCommit string, posterID int64, title string) *issues_model.PullRequest { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: posterID}) issue := &issues_model.Issue{ @@ -67,7 +103,7 @@ func createMergedPullRequest(t *testing.T, repo *repo_model.Repository, mergeCom Repo: repo, Poster: user, PosterID: user.ID, - Title: "Release notes test pull request", + Title: title, Content: "content", } diff --git a/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go index 93e83b51de5..2b781185564 100644 --- a/services/repository/archiver/archiver.go +++ b/services/repository/archiver/archiver.go @@ -56,13 +56,13 @@ func NewRequest(repo *repo_model.Repository, gitRepo *git.Repository, archiveRef } // Get corresponding commit. - commitID, err := gitRepo.ConvertToGitID(archiveRefShortName) + commit, err := gitRepo.GetCommit(archiveRefShortName) if err != nil { return nil, util.NewNotExistErrorf("unrecognized repository reference: %s", archiveRefShortName) } r := &ArchiveRequest{Repo: repo, archiveRefShortName: archiveRefShortName, Type: archiveType, Paths: paths} - r.CommitID = commitID.String() + r.CommitID = commit.ID.String() return r, nil } @@ -330,7 +330,7 @@ func ServeRepoArchive(ctx *gitea_context.Base, archiveReq *ArchiveRequest) error // because errors may happen in git command and such cases aren't in our control. httplib.ServeSetHeaders(ctx.Resp, httplib.ServeHeaderOptions{Filename: downloadName}) if err := archiveReq.Stream(ctx, ctx.Resp); err != nil && !ctx.Written() { - if gitcmd.StderrHasPrefix(err, "fatal: pathspec") { + if gitcmd.IsStderr(err, gitcmd.StderrPathSpec) || gitcmd.IsStderr(err, gitcmd.StderrNotTreeObject) { return util.NewInvalidArgumentErrorf("path doesn't exist or is invalid") } return fmt.Errorf("archive repo %s: failed to stream: %w", archiveReq.Repo.FullName(), err) diff --git a/services/repository/delete.go b/services/repository/delete.go index d1b41f980bc..0666a1616b5 100644 --- a/services/repository/delete.go +++ b/services/repository/delete.go @@ -177,6 +177,7 @@ func DeleteRepositoryDirectly(ctx context.Context, repoID int64, ignoreOrgTeams &actions_model.ActionScheduleSpec{RepoID: repoID}, &actions_model.ActionSchedule{RepoID: repoID}, &actions_model.ActionArtifact{RepoID: repoID}, + &actions_model.ActionRunJobSummary{RepoID: repoID}, &actions_model.ActionRunnerToken{RepoID: repoID}, &issues_model.IssuePin{RepoID: repoID}, ); err != nil { diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index b36f5f192a8..553f4232e2a 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -300,12 +300,7 @@ func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *Commit cmdCommitTree.AddOptionFormat("-S%s", key.KeyID) if t.repo.GetTrustModel() == repo_model.CommitterTrustModel || t.repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { if committerSig.Name != authorSig.Name || committerSig.Email != authorSig.Email { - // Add trailers - _, _ = messageBytes.WriteString("\n") - _, _ = messageBytes.WriteString("Co-authored-by: ") - _, _ = messageBytes.WriteString(committerSig.String()) - _, _ = messageBytes.WriteString("\n") - _, _ = messageBytes.WriteString("Co-committed-by: ") + _, _ = messageBytes.WriteString("\n" + git.CoAuthoredByTrailer + ": ") _, _ = messageBytes.WriteString(committerSig.String()) _, _ = messageBytes.WriteString("\n") } diff --git a/services/repository/gitgraph/graph_models.go b/services/repository/gitgraph/graph_models.go index 82092f71f36..99f8222ca7c 100644 --- a/services/repository/gitgraph/graph_models.go +++ b/services/repository/gitgraph/graph_models.go @@ -13,8 +13,10 @@ import ( 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" user_model "gitea.dev/models/user" + "gitea.dev/modules/container" "gitea.dev/modules/git" "gitea.dev/modules/log" asymkey_service "gitea.dev/services/asymkey" @@ -93,9 +95,7 @@ func (graph *Graph) AddCommit(row, column int, flowID int64, data []byte) error // before finally retrieving the latest status func (graph *Graph) LoadAndProcessCommits(ctx context.Context, repository *repo_model.Repository, gitRepo *git.Repository) error { var err error - var ok bool - - emails := map[string]*user_model.User{} + emailSet := make(container.Set[string]) keyMap := map[string]bool{} for _, c := range graph.Commits { @@ -106,14 +106,26 @@ func (graph *Graph) LoadAndProcessCommits(ctx context.Context, repository *repo_ if err != nil { return fmt.Errorf("GetCommit: %s Error: %w", c.Rev, err) } - if c.Commit.Author != nil { - email := c.Commit.Author.Email - if c.User, ok = emails[email]; !ok { - c.User, _ = user_model.GetUserByEmail(ctx, email) - emails[email] = c.User - } + emailSet.Add(c.Commit.Author.Email) } + for _, sig := range c.Commit.AllParticipantIdentities() { + emailSet.Add(sig.Email) + } + } + + emailUserMap, err := user_model.GetUsersByEmails(ctx, emailSet.Values()) + if err != nil { + log.Error("GetUsersByEmails: %v", err) + } + + for _, c := range graph.Commits { + if c.Commit == nil { + continue + } + + c.User = emailUserMap.GetByEmail(c.Commit.Author.Email) + c.AvatarStackData = gituser.BuildAvatarStackData(ctx, c.Commit.AllParticipantIdentities(), emailUserMap) c.Verification = asymkey_service.ParseCommitWithSignature(ctx, c.Commit) @@ -246,18 +258,19 @@ func newRefsFromRefNames(refNames []byte) []git.Reference { // Commit represents a commit at coordinate X, Y with the data type Commit struct { - Commit *git.Commit - User *user_model.User - Verification *asymkey_model.CommitVerification - Status *git_model.CommitStatus - Flow int64 - Row int - Column int - Refs []git.Reference - Rev string - Date time.Time - ShortRev string - Subject string + Commit *git.Commit + User *user_model.User // author + AvatarStackData *gituser.AvatarStackData + Verification *asymkey_model.CommitVerification + Status *git_model.CommitStatus + Flow int64 + Row int + Column int + Refs []git.Reference + Rev string + Date time.Time // author date from "%ad" + ShortRev string + Subject string } // OnlyRelation returns whether this a relation only commit diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index 9fcc5750ec9..d1006abf7e8 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -7,7 +7,6 @@ package wiki import ( "context" "fmt" - "os" "gitea.dev/models/db" repo_model "gitea.dev/models/repo" @@ -21,6 +20,7 @@ import ( "gitea.dev/modules/graceful" "gitea.dev/modules/log" repo_module "gitea.dev/modules/repository" + "gitea.dev/modules/util" asymkey_service "gitea.dev/services/asymkey" repo_service "gitea.dev/services/repository" ) @@ -59,7 +59,7 @@ func prepareGitPath(gitRepo *git.Repository, defaultWikiBranch string, wikiPath // Look for both files filesInIndex, err := gitRepo.LsTree(defaultWikiBranch, unescaped, gitPath) if err != nil { - if gitcmd.IsStdErrorNotValidObjectName(err) { + if gitcmd.IsStderr(err, gitcmd.StderrNotValidObjectName) { return false, gitPath, nil // branch doesn't exist } log.Error("Wiki LsTree failed, err: %v", err) @@ -304,7 +304,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model return err } } else { - return os.ErrNotExist + return util.ErrNotExist } // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here diff --git a/templates/devtest/avatar-stack.tmpl b/templates/devtest/avatar-stack.tmpl new file mode 100644 index 00000000000..12d1ff953bf --- /dev/null +++ b/templates/devtest/avatar-stack.tmpl @@ -0,0 +1,18 @@ +{{template "devtest/devtest-header"}} +
+
+

Avatar Stack

+ + + + {{range $s := .AvatarStackScenarios}} + + + + + {{end}} + +
ScenarioRendered
{{$s.Label}}{{ctx.RenderUtils.AvatarStackWithNames $s.Data}}
+
+
+{{template "devtest/devtest-footer"}} diff --git a/templates/devtest/badge-commit-sign.tmpl b/templates/devtest/badge-commit-sign.tmpl index a6677c44958..8cfb63a0832 100644 --- a/templates/devtest/badge-commit-sign.tmpl +++ b/templates/devtest/badge-commit-sign.tmpl @@ -4,7 +4,7 @@

Commit Sign Badges

{{range $commit := .MockCommits}}
- {{template "repo/commit_sign_badge" dict "Commit" $commit "CommitBaseLink" "/devtest/commit" "CommitSignVerification" $commit.Verification}} + {{template "repo/commit_sign_badge" dict "Commit" $commit.GitCommit "CommitBaseLink" "/devtest/commit" "CommitSignVerification" $commit.Verification}} {{template "repo/commit_sign_badge" dict "CommitSignVerification" $commit.Verification}}
{{end}} diff --git a/templates/devtest/repo-action-view.tmpl b/templates/devtest/repo-action-view.tmpl index 4e06f72cdd9..12da63a7e9e 100644 --- a/templates/devtest/repo-action-view.tmpl +++ b/templates/devtest/repo-action-view.tmpl @@ -1,11 +1,11 @@ {{template "base/head" .}}
{{template "repo/actions/view_component" (dict "JobID" (or .JobID 0) diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index 346f4002b40..9fedd05f949 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -41,26 +41,26 @@
-
+
{{/* the milestone-list class is kept because many tests depend on it */}} {{range .Projects}} -
  • -

    +
    + {{svg .IconName 16}} - {{.Title}} -

    -
    -
    -
    + {{.Title}} + +
    +
    +
    {{svg "octicon-issue-opened" 14}} {{ctx.Locale.PrettyNumber .NumOpenIssues}} {{ctx.Locale.Tr "repo.issues.open_title"}}
    -
    +
    {{svg "octicon-check" 14}} {{ctx.Locale.PrettyNumber .NumClosedIssues}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
    {{if and $.CanWriteProjects (not $.Repository.IsArchived)}} -
    +
    {{svg "octicon-pencil" 14}}{{ctx.Locale.Tr "repo.issues.label_edit"}} {{if .IsClosed}} {{svg "octicon-check" 14}}{{ctx.Locale.Tr "repo.projects.open"}} @@ -74,7 +74,7 @@ {{if .Description}}
    {{.RenderedContent}}
    {{end}} -
  • +
    {{else}} {{if and (eq .OpenCount 0) (eq .ClosedCount 0)}}
    diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl index edc167e706b..58dc222cf5b 100644 --- a/templates/repo/actions/runs_list.tmpl +++ b/templates/repo/actions/runs_list.tmpl @@ -13,13 +13,10 @@
    - - {{if $run.Title}} - {{ctx.RenderUtils.RenderCommitMessageLinkSubject $run.Title $run.Link $.Repository}} - {{else}} - {{ctx.Locale.Tr "actions.runs.empty_commit_message"}} - {{end}} - +
    + {{$title := or $run.Title (ctx.Locale.Tr "actions.runs.empty_commit_message")}} + {{ctx.RenderUtils.RenderCommitMessageLinkSubject $title $run.Link $.Repository}} +
    {{$workflowName := index $.WorkflowNames $run.WorkflowID}} {{if not $.CurWorkflow}}{{if $workflowName}}{{$workflowName}}{{else}}{{$run.WorkflowID}}{{end}} {{end}}#{{$run.Index}}: diff --git a/templates/repo/actions/view_component.tmpl b/templates/repo/actions/view_component.tmpl index 2ed47ad9df8..0d508e69e55 100644 --- a/templates/repo/actions/view_component.tmpl +++ b/templates/repo/actions/view_component.tmpl @@ -15,9 +15,14 @@ data-locale-runs-pushed-by="{{ctx.Locale.Tr "actions.runs.pushed_by"}}" data-locale-summary="{{ctx.Locale.Tr "actions.runs.summary"}}" data-locale-all-jobs="{{ctx.Locale.Tr "actions.runs.all_jobs"}}" + data-locale-job-summaries="{{ctx.Locale.Tr "actions.runs.job_summaries"}}" data-locale-expand-caller-jobs="{{ctx.Locale.Tr "actions.runs.expand_caller_jobs"}}" data-locale-collapse-caller-jobs="{{ctx.Locale.Tr "actions.runs.collapse_caller_jobs"}}" data-locale-triggered-via="{{ctx.Locale.Tr "actions.runs.triggered_via"}}" + data-locale-rerun-triggered="{{ctx.Locale.Tr "actions.runs.rerun_triggered"}}" + data-locale-back-to-pull-request="{{ctx.Locale.Tr "actions.runs.back_to_pull_request"}}" + data-locale-back-to-workflow="{{ctx.Locale.Tr "actions.runs.back_to_workflow"}}" + data-locale-status-label="{{ctx.Locale.Tr "actions.runs.status"}}" 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"}}" diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl index 8bdefa5d43e..d108ea33797 100644 --- a/templates/repo/blame.tmpl +++ b/templates/repo/blame.tmpl @@ -43,7 +43,7 @@
    - {{$row.Avatar}} + {{if $row.AvatarStackData}}{{ctx.RenderUtils.AvatarStack $row.AvatarStackData}}{{end}}
    - - {{DateUtils.TimeSince .Commit.Author.When}} + {{DateUtils.TimeSince .Commit.Committer.When}}
    {{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}} @@ -155,6 +154,29 @@ {{end}}
    + {{if .CommitOtherParticipants}} +
    + {{ctx.Locale.Tr "repo.diff.coauthored_by"}} + {{range $participant := .CommitOtherParticipants}} + {{$user := $participant.GiteaUser}} + {{$gitIdentity := $participant.GitIdentity}} + {{if $user}} + {{ctx.AvatarUtils.Avatar $user 20}} + {{$user.GetDisplayName}} + {{else}} + {{$gitName := $gitIdentity.Name}} + {{$gitEmail := $gitIdentity.Email}} + {{ctx.AvatarUtils.AvatarByEmail $gitEmail $gitName 20}} + {{if $gitEmail}} + {{$gitName}} + {{else}} + {{$gitName}} + {{end}} + {{end}} + {{end}} +
    + {{end}} + {{if .Verification}} {{template "repo/commit_sign_badge" dict "CommitSignVerification" .Verification}} {{end}} diff --git a/templates/repo/commit_sign_badge.tmpl b/templates/repo/commit_sign_badge.tmpl index f63e4ec899d..bf8185fd0b5 100644 --- a/templates/repo/commit_sign_badge.tmpl +++ b/templates/repo/commit_sign_badge.tmpl @@ -64,10 +64,10 @@ so this template should be kept as small as possible, DO NOT put large component {{- if $verified -}} {{- if and $signingUser $signingUser.ID -}} {{svg "gitea-lock"}} - {{ctx.AvatarUtils.Avatar $signingUser 16}} + {{ctx.AvatarUtils.Avatar $signingUser 20}} {{- else -}} {{svg "gitea-lock-cog"}} - {{ctx.AvatarUtils.AvatarByEmail $signingEmail "" 16}} + {{ctx.AvatarUtils.AvatarByEmail $signingEmail "" 20}} {{- end -}} {{- else -}} {{svg "gitea-unlock"}} diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl index e79d189b8d3..7a99bc7ac2e 100644 --- a/templates/repo/commits_list.tmpl +++ b/templates/repo/commits_list.tmpl @@ -2,27 +2,21 @@ - + - + {{$commitRepoLink := $.RepoLink}}{{if $.CommitRepoLink}}{{$commitRepoLink = $.CommitRepoLink}}{{end}} - {{range $commit := .Commits}} + {{range $commit := $.Commits}} + {{$gitCommit := $commit.GitCommit}} + {{$commitID := $gitCommit.ID.String}} - {{if .Committer}} - - {{else}} - - {{end}} +
    {{ctx.Locale.Tr "repo.commits.author"}}{{ctx.Locale.Tr "repo.commits.author"}} {{StringUtils.ToUpper $.Repository.ObjectFormatName}}{{ctx.Locale.Tr "repo.commits.message"}}{{ctx.Locale.Tr "repo.commits.message"}} {{ctx.Locale.Tr "repo.commits.date"}}
    - - {{- if .User -}} - {{- ctx.AvatarUtils.Avatar .User 20 "tw-mr-2" -}} - {{- .User.GetShortDisplayNameLinkHTML -}} - {{- else -}} - {{- ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20 "tw-mr-2" -}} - {{- .Author.Name -}} - {{- end -}} - + {{ctx.RenderUtils.AvatarStackWithNames $commit.AvatarStackData}} {{$commitBaseLink := ""}} @@ -33,52 +27,48 @@ {{else}} {{$commitBaseLink = printf "%s/commit" $commitRepoLink}} {{end}} - {{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}} + {{template "repo/commit_sign_badge" dict "Commit" $gitCommit "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}} {{if $.PageIsWiki}} - - {{$commit.MessageTitle | ctx.RenderUtils.RenderEmoji}} + + {{$gitCommit.MessageTitle | ctx.RenderUtils.RenderEmoji}} {{else}} - {{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape $commit.ID.String)}} - - {{ctx.RenderUtils.RenderCommitMessageLinkSubject $commit.MessageUTF8 $commitLink $.Repository}} + {{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape $commitID)}} + + {{ctx.RenderUtils.RenderCommitMessageLinkSubject $gitCommit.MessageUTF8 $commitLink $.Repository}} {{end}} - {{if $commit.MessageBody}} + {{if $gitCommit.MessageBody}} {{end}} {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} - {{if $commit.MessageBody}} -
    {{ctx.RenderUtils.RenderCommitBody $commit.MessageUTF8 $.Repository}}
    + {{if $gitCommit.MessageBody}} +
    {{ctx.RenderUtils.RenderCommitBody $gitCommit.MessageUTF8 $.Repository}}
    {{end}} {{if $.CommitsTagsMap}} - {{range (index $.CommitsTagsMap .ID.String)}} + {{range (index $.CommitsTagsMap $commitID)}} {{- template "repo/tag/name" dict "AdditionalClasses" "tw-py-0" "RepoLink" $.Repository.Link "TagName" .TagName "IsRelease" (not .IsTag) -}} {{end}} {{end}}
    {{DateUtils.TimeSince .Committer.When}}{{DateUtils.TimeSince .Author.When}}{{DateUtils.TimeSince $gitCommit.Committer.When}} - + {{/* at the moment, wiki doesn't support these "view" links like "view at history point" */}} {{if not $.PageIsWiki}} {{/* view single file diff */}} {{if $.FileTreePath}} {{svg "octicon-file-diff"}} {{end}} {{/* view at history point */}} - {{$viewCommitLink := printf "%s/src/commit/%s" $commitRepoLink (PathEscape .ID.String)}} + {{$viewCommitLink := printf "%s/src/commit/%s" $commitRepoLink (PathEscape $commitID)}} {{if $.FileTreePath}}{{$viewCommitLink = printf "%s/%s" $viewCommitLink (PathEscapeSegments $.FileTreePath)}}{{end}} {{svg "octicon-file-code"}} {{end}} diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl index c5f0d5b5900..2dd6727e93c 100644 --- a/templates/repo/commits_list_small.tmpl +++ b/templates/repo/commits_list_small.tmpl @@ -1,35 +1,32 @@ {{$index := 0}}
    -{{range $commit := .comment.Commits}} +{{range $commit := $.comment.Commits}} + {{$gitCommit := $commit.GitCommit}} {{$tag := printf "%s-%d" $.comment.HashTag $index}} {{$index = Eval $index "+" 1}}
    {{/*singular-commit*/}} {{svg "octicon-git-commit"}} - {{if .User}} - {{ctx.AvatarUtils.Avatar .User 20}} - {{else}} - {{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20}} - {{end}} + {{ctx.RenderUtils.AvatarStack $commit.AvatarStackData}} {{$commitBaseLink := printf "%s/commit" $.comment.Issue.PullRequest.BaseRepo.Link}} - {{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}} + {{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape $gitCommit.ID.String)}} - - {{- ctx.RenderUtils.RenderCommitMessageLinkSubject $commit.MessageUTF8 $commitLink $.comment.Issue.PullRequest.BaseRepo -}} + + {{- ctx.RenderUtils.RenderCommitMessageLinkSubject $gitCommit.MessageUTF8 $commitLink $.comment.Issue.PullRequest.BaseRepo -}} - {{if $commit.MessageBody}} + {{if $gitCommit.MessageBody}} {{end}} - {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} - {{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}} + {{template "repo/commit_statuses" dict "Status" $commit.Status "Statuses" $commit.Statuses}} + {{template "repo/commit_sign_badge" dict "Commit" $gitCommit "CommitBaseLink" $commitBaseLink "CommitSignVerification" $commit.Verification}}
    - {{if $commit.MessageBody}} + {{if $gitCommit.MessageBody}}
    -		{{- ctx.RenderUtils.RenderCommitBody $commit.MessageUTF8 $.comment.Issue.PullRequest.BaseRepo -}}
    +		{{- ctx.RenderUtils.RenderCommitBody $gitCommit.MessageUTF8 $.comment.Issue.PullRequest.BaseRepo -}}
     	
    {{end}} {{end}} diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl index 56a4867ff4b..59e87d10c03 100644 --- a/templates/repo/commits_table.tmpl +++ b/templates/repo/commits_table.tmpl @@ -1,21 +1,30 @@ -

    -
    - {{if or .PageIsCommits (gt .CommitCount 0)}} - {{.CommitCount}} {{ctx.Locale.Tr "repo.commits.commits"}} +
    +
    + {{if .Commits}} + {{if gt .CommitCount 0}}{{.CommitCount}}{{end}}{{/* CommitCount is -1 when "follow rename" */}} + {{ctx.Locale.Tr "repo.commits.commits"}} {{else if .IsNothingToCompare}} {{ctx.Locale.Tr "repo.commits.nothing_to_compare"}} {{else}} {{ctx.Locale.Tr "repo.commits.no_commits" $.BaseBranch $.HeadBranch}} {{end}}
    - {{if .IsDiffCompare}} -
    +
    + {{if .ShowFollowRename}} +
    + + +
    + {{end}} + {{if .IsDiffCompare}} + - {{end}} -

    + {{end}} +
    + {{if .PageIsCommits}}
    @@ -29,7 +38,7 @@
    {{end}} -{{if and .Commits (gt .CommitCount 0)}} +{{if .Commits}} {{template "repo/commits_list" .}} {{end}} diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl index d86f73fe654..b9288da911c 100644 --- a/templates/repo/graph/commits.tmpl +++ b/templates/repo/graph/commits.tmpl @@ -41,14 +41,7 @@ - {{if $commit.User}} - {{ctx.AvatarUtils.Avatar $commit.User 18}} - {{$commit.User.GetShortDisplayNameLinkHTML}} - {{else}} - {{$gitUserName := $commit.Commit.Author.Name}} - {{ctx.AvatarUtils.AvatarByEmail $commit.Commit.Author.Email $gitUserName 18}} - {{$gitUserName}} - {{end}} + {{ctx.RenderUtils.AvatarStackWithNames $commit.AvatarStackData}} {{DateUtils.FullTime $commit.Date}} diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl index 90e2a8ae3b9..4af4697b44f 100644 --- a/templates/repo/issue/milestones.tmpl +++ b/templates/repo/issue/milestones.tmpl @@ -15,42 +15,42 @@ {{template "repo/issue/filters" .}} -
    +
    {{range .Milestones}} -
  • -
    -

    +
    +
    + {{svg "octicon-milestone" 16}} {{.Name}} -

    -
    - {{.Completeness}}% - + +
    + {{.Completeness}}% +
    -
    -
    -
    +
    +
    +
    {{svg "octicon-issue-opened" 14}} {{ctx.Locale.PrettyNumber .NumOpenIssues}} {{ctx.Locale.Tr "repo.issues.open_title"}}
    -
    +
    {{svg "octicon-check" 14}} {{ctx.Locale.PrettyNumber .NumClosedIssues}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
    {{if .TotalTrackedTime}} -
    +
    {{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Hour}}
    {{end}} {{if .UpdatedUnix}} -
    +
    {{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.milestones.update_ago" (DateUtils.TimeSince .UpdatedUnix)}}
    {{end}} -
    +
    {{if .IsClosed}} {{$closedDate:= DateUtils.TimeSince .ClosedDateUnix}} {{svg "octicon-clock" 14}} @@ -69,7 +69,7 @@
    {{if and (or $.CanWriteIssues $.CanWritePulls) (not $.Repository.IsArchived)}} -
  • +
    {{end}} {{template "base/paginate" .}} diff --git a/templates/repo/latest_commit.tmpl b/templates/repo/latest_commit.tmpl index c0518189b85..b2aebf0d42c 100644 --- a/templates/repo/latest_commit.tmpl +++ b/templates/repo/latest_commit.tmpl @@ -2,15 +2,7 @@ {{if not .LatestCommit}} … {{else}} - - {{- if .LatestCommitUser -}} - {{- ctx.AvatarUtils.Avatar .LatestCommitUser 20 "tw-mr-2" -}} - {{.LatestCommitUser.GetShortDisplayNameLinkHTML}} - {{- else if .LatestCommit.Author -}} - {{- ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 20 "tw-mr-2" -}} - {{.LatestCommit.Author.Name}} - {{- end -}} - + {{ctx.RenderUtils.AvatarStackWithNames .LatestCommitAvatarStackData}} {{template "repo/commit_sign_badge" dict "Commit" .LatestCommit "CommitBaseLink" (print .RepoLink "/commit") "CommitSignVerification" .LatestCommitVerification}} diff --git a/templates/repo/wiki/revision.tmpl b/templates/repo/wiki/revision.tmpl index c59047a6404..a9df43ea468 100644 --- a/templates/repo/wiki/revision.tmpl +++ b/templates/repo/wiki/revision.tmpl @@ -9,8 +9,8 @@
    {{$title}}
    - {{$timeSince := DateUtils.TimeSince .Author.When}} - {{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}} + {{$timeSince := DateUtils.TimeSince .Committer.When}} + {{ctx.Locale.Tr "repo.wiki.last_commit_info" .Committer.Name $timeSince}}
    diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index 4c7ef364d29..967a8814c96 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -37,8 +37,8 @@
    {{$title}}
    - {{$timeSince := DateUtils.TimeSince .Author.When}} - {{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}} + {{$timeSince := DateUtils.TimeSince .Committer.When}} + {{ctx.Locale.Tr "repo.wiki.last_commit_info" .Committer.Name $timeSince}}
    diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index 11c4210b460..838caeb7b28 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -16,7 +16,7 @@
    - {{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}} + {{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}} {{if .IsPull}} {{if (index $.CommitStatuses .PullRequest.ID)}} {{/* make the "flex" children align with parent "inline" */}} @@ -37,14 +37,16 @@
    {{end}}
    -
    - +
    + {{if eq $.listType "dashboard"}} {{.Repo.FullName}}#{{.Index}} {{else}} #{{.Index}} {{end}} + +
    {{$timeStr := DateUtils.TimeSince .GetLastEventTimestamp}} {{if .OriginalAuthor}} {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .OriginalAuthor}} @@ -53,6 +55,8 @@ {{else}} {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .Poster.GetDisplayName}} {{end}} +
    + {{if .IsPull}}
    @@ -99,11 +103,9 @@ {{end}} {{if ne .DeadlineUnix 0}} - - - {{svg "octicon-calendar" 14}} - {{DateUtils.AbsoluteShort .DeadlineUnix}} - + + {{svg "octicon-calendar" 14}} + {{DateUtils.AbsoluteShort .DeadlineUnix}} {{end}} {{if .IsPull}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 20c7abd3454..0463c31d2a7 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -7158,6 +7158,12 @@ "description": "page size of results", "name": "limit", "in": "query" + }, + { + "type": "string", + "description": "branch name substring to filter by", + "name": "q", + "in": "query" } ], "responses": { @@ -22989,7 +22995,7 @@ "x-go-name": "SHA" }, "state": { - "description": "State is the overall combined status state\npending CommitStatusPending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped CommitStatusSkipped is for when CommitStatus is Skipped", + "description": "State is the overall combined status state\npending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped is for when CommitStatus is Skipped", "type": "string", "enum": [ "pending", @@ -22999,7 +23005,7 @@ "warning", "skipped" ], - "x-go-enum-desc": "pending CommitStatusPending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped CommitStatusSkipped is for when CommitStatus is Skipped", + "x-go-enum-desc": "pending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped is for when CommitStatus is Skipped", "x-go-name": "State" }, "statuses": { @@ -23254,7 +23260,7 @@ "x-go-name": "ID" }, "status": { - "description": "State represents the status state (pending, success, error, failure)\npending CommitStatusPending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped CommitStatusSkipped is for when CommitStatus is Skipped", + "description": "State represents the status state (pending, success, error, failure)\npending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped is for when CommitStatus is Skipped", "type": "string", "enum": [ "pending", @@ -23264,7 +23270,7 @@ "warning", "skipped" ], - "x-go-enum-desc": "pending CommitStatusPending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped CommitStatusSkipped is for when CommitStatus is Skipped", + "x-go-enum-desc": "pending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped is for when CommitStatus is Skipped", "x-go-name": "State" }, "target_url": { @@ -24307,7 +24313,7 @@ "REQUEST_CHANGES", "REQUEST_REVIEW" ], - "x-go-enum-desc": "APPROVED ReviewStateApproved ReviewStateApproved pr is approved\nPENDING ReviewStatePending ReviewStatePending pr state is pending\nCOMMENT ReviewStateComment ReviewStateComment is a comment review\nREQUEST_CHANGES ReviewStateRequestChanges ReviewStateRequestChanges changes for pr are requested\nREQUEST_REVIEW ReviewStateRequestReview ReviewStateRequestReview review is requested from user", + "x-go-enum-desc": "APPROVED ReviewStateApproved pr is approved\nPENDING ReviewStatePending pr state is pending\nCOMMENT ReviewStateComment is a comment review\nREQUEST_CHANGES ReviewStateRequestChanges changes for pr are requested\nREQUEST_REVIEW ReviewStateRequestReview review is requested from user", "x-go-name": "Event" } }, @@ -24486,7 +24492,7 @@ "x-go-name": "Description" }, "state": { - "description": "State represents the status state to set (pending, success, error, failure)\npending CommitStatusPending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped CommitStatusSkipped is for when CommitStatus is Skipped", + "description": "State represents the status state to set (pending, success, error, failure)\npending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped is for when CommitStatus is Skipped", "type": "string", "enum": [ "pending", @@ -24496,7 +24502,7 @@ "warning", "skipped" ], - "x-go-enum-desc": "pending CommitStatusPending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped CommitStatusSkipped is for when CommitStatus is Skipped", + "x-go-enum-desc": "pending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped is for when CommitStatus is Skipped", "x-go-name": "State" }, "target_url": { @@ -26805,7 +26811,7 @@ "open", "closed" ], - "x-go-enum-desc": "open StateOpen StateOpen pr is opened\nclosed StateClosed StateClosed pr is closed", + "x-go-enum-desc": "open StateOpen pr is opened\nclosed StateClosed pr is closed", "x-go-name": "State" }, "time_estimate": { @@ -27163,19 +27169,19 @@ "type": "object", "properties": { "Context": { - "description": "URL path for rendering issue, media and file links\nExpected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}\n\nin: body", + "description": "URL path for rendering issue, media and file links\nExpected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}", "type": "string" }, "Mode": { - "description": "Mode to render (markdown, comment, wiki, file)\n\nin: body", + "description": "Mode to render (markdown, comment, wiki, file)", "type": "string" }, "Text": { - "description": "Text markdown to render\n\nin: body", + "description": "Text markdown to render", "type": "string" }, "Wiki": { - "description": "Is it a wiki page? (use mode=wiki instead)\n\nDeprecated: true\nin: body", + "description": "Is it a wiki page? (use mode=wiki instead)\n\nDeprecated: true", "type": "boolean" } }, @@ -27186,23 +27192,23 @@ "type": "object", "properties": { "Context": { - "description": "URL path for rendering issue, media and file links\nExpected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}\n\nin: body", + "description": "URL path for rendering issue, media and file links\nExpected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}", "type": "string" }, "FilePath": { - "description": "File path for detecting extension in file mode\n\nin: body", + "description": "File path for detecting extension in file mode", "type": "string" }, "Mode": { - "description": "Mode to render (markdown, comment, wiki, file)\n\nin: body", + "description": "Mode to render (markdown, comment, wiki, file)", "type": "string" }, "Text": { - "description": "Text markup to render\n\nin: body", + "description": "Text markup to render", "type": "string" }, "Wiki": { - "description": "Is it a wiki page? (use mode=wiki instead)\n\nDeprecated: true\nin: body", + "description": "Is it a wiki page? (use mode=wiki instead)\n\nDeprecated: true", "type": "boolean" } }, @@ -27439,13 +27445,13 @@ "x-go-name": "OpenIssues" }, "state": { - "description": "State indicates if the milestone is open or closed\nopen StateOpen StateOpen pr is opened\nclosed StateClosed StateClosed pr is closed", + "description": "State indicates if the milestone is open or closed\nopen StateOpen pr is opened\nclosed StateClosed pr is closed", "type": "string", "enum": [ "open", "closed" ], - "x-go-enum-desc": "open StateOpen StateOpen pr is opened\nclosed StateClosed StateClosed pr is closed", + "x-go-enum-desc": "open StateOpen pr is opened\nclosed StateClosed pr is closed", "x-go-name": "State" }, "title": { @@ -27660,14 +27666,14 @@ "x-go-name": "LatestCommentURL" }, "state": { - "description": "State indicates the current state of the notification subject\nopen NotifySubjectStateOpen NotifySubjectStateOpen is an open subject\nclosed NotifySubjectStateClosed NotifySubjectStateClosed is a closed subject\nmerged NotifySubjectStateMerged NotifySubjectStateMerged is a merged pull request", + "description": "State indicates the current state of the notification subject\nopen NotifySubjectStateOpen is an open subject\nclosed NotifySubjectStateClosed is a closed subject\nmerged NotifySubjectStateMerged is a merged pull request", "type": "string", "enum": [ "open", "closed", "merged" ], - "x-go-enum-desc": "open NotifySubjectStateOpen NotifySubjectStateOpen is an open subject\nclosed NotifySubjectStateClosed NotifySubjectStateClosed is a closed subject\nmerged NotifySubjectStateMerged NotifySubjectStateMerged is a merged pull request", + "x-go-enum-desc": "open NotifySubjectStateOpen is an open subject\nclosed NotifySubjectStateClosed is a closed subject\nmerged NotifySubjectStateMerged is a merged pull request", "x-go-name": "State" }, "title": { @@ -27676,7 +27682,7 @@ "x-go-name": "Title" }, "type": { - "description": "Type indicates the type of the notification subject\nIssue NotifySubjectIssue NotifySubjectIssue a issue is subject of an notification\nPull NotifySubjectPull NotifySubjectPull a pull is subject of an notification\nCommit NotifySubjectCommit NotifySubjectCommit a commit is subject of an notification\nRepository NotifySubjectRepository NotifySubjectRepository a repository is subject of an notification", + "description": "Type indicates the type of the notification subject\nIssue NotifySubjectIssue a issue is subject of an notification\nPull NotifySubjectPull a pull is subject of an notification\nCommit NotifySubjectCommit a commit is subject of an notification\nRepository NotifySubjectRepository a repository is subject of an notification", "type": "string", "enum": [ "Issue", @@ -27684,7 +27690,7 @@ "Commit", "Repository" ], - "x-go-enum-desc": "Issue NotifySubjectIssue NotifySubjectIssue a issue is subject of an notification\nPull NotifySubjectPull NotifySubjectPull a pull is subject of an notification\nCommit NotifySubjectCommit NotifySubjectCommit a commit is subject of an notification\nRepository NotifySubjectRepository NotifySubjectRepository a repository is subject of an notification", + "x-go-enum-desc": "Issue NotifySubjectIssue a issue is subject of an notification\nPull NotifySubjectPull a pull is subject of an notification\nCommit NotifySubjectCommit a commit is subject of an notification\nRepository NotifySubjectRepository a repository is subject of an notification", "x-go-name": "Type" }, "url": { @@ -28440,13 +28446,13 @@ "x-go-name": "ReviewComments" }, "state": { - "description": "The current state of the pull request\nopen StateOpen StateOpen pr is opened\nclosed StateClosed StateClosed pr is closed", + "description": "The current state of the pull request\nopen StateOpen pr is opened\nclosed StateClosed pr is closed", "type": "string", "enum": [ "open", "closed" ], - "x-go-enum-desc": "open StateOpen StateOpen pr is opened\nclosed StateClosed StateClosed pr is closed", + "x-go-enum-desc": "open StateOpen pr is opened\nclosed StateClosed pr is closed", "x-go-name": "State" }, "title": { @@ -28547,7 +28553,7 @@ "REQUEST_CHANGES", "REQUEST_REVIEW" ], - "x-go-enum-desc": "APPROVED ReviewStateApproved ReviewStateApproved pr is approved\nPENDING ReviewStatePending ReviewStatePending pr state is pending\nCOMMENT ReviewStateComment ReviewStateComment is a comment review\nREQUEST_CHANGES ReviewStateRequestChanges ReviewStateRequestChanges changes for pr are requested\nREQUEST_REVIEW ReviewStateRequestReview ReviewStateRequestReview review is requested from user", + "x-go-enum-desc": "APPROVED ReviewStateApproved pr is approved\nPENDING ReviewStatePending pr state is pending\nCOMMENT ReviewStateComment is a comment review\nREQUEST_CHANGES ReviewStateRequestChanges changes for pr are requested\nREQUEST_REVIEW ReviewStateRequestReview review is requested from user", "x-go-name": "State" }, "submitted_at": { @@ -29442,7 +29448,7 @@ "REQUEST_CHANGES", "REQUEST_REVIEW" ], - "x-go-enum-desc": "APPROVED ReviewStateApproved ReviewStateApproved pr is approved\nPENDING ReviewStatePending ReviewStatePending pr state is pending\nCOMMENT ReviewStateComment ReviewStateComment is a comment review\nREQUEST_CHANGES ReviewStateRequestChanges ReviewStateRequestChanges changes for pr are requested\nREQUEST_REVIEW ReviewStateRequestReview ReviewStateRequestReview review is requested from user", + "x-go-enum-desc": "APPROVED ReviewStateApproved pr is approved\nPENDING ReviewStatePending pr state is pending\nCOMMENT ReviewStateComment is a comment review\nREQUEST_CHANGES ReviewStateRequestChanges changes for pr are requested\nREQUEST_REVIEW ReviewStateRequestReview review is requested from user", "x-go-name": "Event" } }, diff --git a/templates/swagger/v1_openapi3_json.tmpl b/templates/swagger/v1_openapi3_json.tmpl index 0a15c8eba49..fcc1603ce11 100644 --- a/templates/swagger/v1_openapi3_json.tmpl +++ b/templates/swagger/v1_openapi3_json.tmpl @@ -3179,7 +3179,7 @@ "$ref": "#/components/schemas/CommitStatusState" } ], - "description": "State is the overall combined status state\npending CommitStatusPending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped CommitStatusSkipped is for when CommitStatus is Skipped" + "description": "State is the overall combined status state\npending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped is for when CommitStatus is Skipped" }, "statuses": { "description": "Statuses contains all individual commit statuses", @@ -3445,7 +3445,7 @@ "$ref": "#/components/schemas/CommitStatusState" } ], - "description": "State represents the status state (pending, success, error, failure)\npending CommitStatusPending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped CommitStatusSkipped is for when CommitStatus is Skipped" + "description": "State represents the status state (pending, success, error, failure)\npending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped is for when CommitStatus is Skipped" }, "target_url": { "description": "TargetURL is the URL to link to for more details", @@ -4673,7 +4673,7 @@ "$ref": "#/components/schemas/CommitStatusState" } ], - "description": "State represents the status state to set (pending, success, error, failure)\npending CommitStatusPending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped CommitStatusSkipped is for when CommitStatus is Skipped" + "description": "State represents the status state to set (pending, success, error, failure)\npending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped is for when CommitStatus is Skipped" }, "target_url": { "description": "TargetURL is the URL to link to for more details", @@ -7326,20 +7326,20 @@ "description": "MarkdownOption markdown options", "properties": { "Context": { - "description": "URL path for rendering issue, media and file links\nExpected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}\n\nin: body", + "description": "URL path for rendering issue, media and file links\nExpected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}", "type": "string" }, "Mode": { - "description": "Mode to render (markdown, comment, wiki, file)\n\nin: body", + "description": "Mode to render (markdown, comment, wiki, file)", "type": "string" }, "Text": { - "description": "Text markdown to render\n\nin: body", + "description": "Text markdown to render", "type": "string" }, "Wiki": { "deprecated": true, - "description": "Is it a wiki page? (use mode=wiki instead)\n\nDeprecated: true\nin: body", + "description": "Is it a wiki page? (use mode=wiki instead)\n\nDeprecated: true", "type": "boolean" } }, @@ -7350,24 +7350,24 @@ "description": "MarkupOption markup options", "properties": { "Context": { - "description": "URL path for rendering issue, media and file links\nExpected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}\n\nin: body", + "description": "URL path for rendering issue, media and file links\nExpected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}", "type": "string" }, "FilePath": { - "description": "File path for detecting extension in file mode\n\nin: body", + "description": "File path for detecting extension in file mode", "type": "string" }, "Mode": { - "description": "Mode to render (markdown, comment, wiki, file)\n\nin: body", + "description": "Mode to render (markdown, comment, wiki, file)", "type": "string" }, "Text": { - "description": "Text markup to render\n\nin: body", + "description": "Text markup to render", "type": "string" }, "Wiki": { "deprecated": true, - "description": "Is it a wiki page? (use mode=wiki instead)\n\nDeprecated: true\nin: body", + "description": "Is it a wiki page? (use mode=wiki instead)\n\nDeprecated: true", "type": "boolean" } }, @@ -7610,7 +7610,7 @@ "$ref": "#/components/schemas/StateType" } ], - "description": "State indicates if the milestone is open or closed\nopen StateOpen StateOpen pr is opened\nclosed StateClosed StateClosed pr is closed" + "description": "State indicates if the milestone is open or closed\nopen StateOpen pr is opened\nclosed StateClosed pr is closed" }, "title": { "description": "Title is the title of the milestone", @@ -7827,14 +7827,14 @@ "x-go-name": "LatestCommentURL" }, "state": { - "description": "State indicates the current state of the notification subject\nopen NotifySubjectStateOpen NotifySubjectStateOpen is an open subject\nclosed NotifySubjectStateClosed NotifySubjectStateClosed is a closed subject\nmerged NotifySubjectStateMerged NotifySubjectStateMerged is a merged pull request", + "description": "State indicates the current state of the notification subject\nopen NotifySubjectStateOpen is an open subject\nclosed NotifySubjectStateClosed is a closed subject\nmerged NotifySubjectStateMerged is a merged pull request", "enum": [ "open", "closed", "merged" ], "type": "string", - "x-go-enum-desc": "open NotifySubjectStateOpen NotifySubjectStateOpen is an open subject\nclosed NotifySubjectStateClosed NotifySubjectStateClosed is a closed subject\nmerged NotifySubjectStateMerged NotifySubjectStateMerged is a merged pull request", + "x-go-enum-desc": "open NotifySubjectStateOpen is an open subject\nclosed NotifySubjectStateClosed is a closed subject\nmerged NotifySubjectStateMerged is a merged pull request", "x-go-name": "State" }, "title": { @@ -7843,7 +7843,7 @@ "x-go-name": "Title" }, "type": { - "description": "Type indicates the type of the notification subject\nIssue NotifySubjectIssue NotifySubjectIssue a issue is subject of an notification\nPull NotifySubjectPull NotifySubjectPull a pull is subject of an notification\nCommit NotifySubjectCommit NotifySubjectCommit a commit is subject of an notification\nRepository NotifySubjectRepository NotifySubjectRepository a repository is subject of an notification", + "description": "Type indicates the type of the notification subject\nIssue NotifySubjectIssue a issue is subject of an notification\nPull NotifySubjectPull a pull is subject of an notification\nCommit NotifySubjectCommit a commit is subject of an notification\nRepository NotifySubjectRepository a repository is subject of an notification", "enum": [ "Issue", "Pull", @@ -7851,7 +7851,7 @@ "Repository" ], "type": "string", - "x-go-enum-desc": "Issue NotifySubjectIssue NotifySubjectIssue a issue is subject of an notification\nPull NotifySubjectPull NotifySubjectPull a pull is subject of an notification\nCommit NotifySubjectCommit NotifySubjectCommit a commit is subject of an notification\nRepository NotifySubjectRepository NotifySubjectRepository a repository is subject of an notification", + "x-go-enum-desc": "Issue NotifySubjectIssue a issue is subject of an notification\nPull NotifySubjectPull a pull is subject of an notification\nCommit NotifySubjectCommit a commit is subject of an notification\nRepository NotifySubjectRepository a repository is subject of an notification", "x-go-name": "Type" }, "url": { @@ -8626,7 +8626,7 @@ "$ref": "#/components/schemas/StateType" } ], - "description": "The current state of the pull request\nopen StateOpen StateOpen pr is opened\nclosed StateClosed StateClosed pr is closed" + "description": "The current state of the pull request\nopen StateOpen pr is opened\nclosed StateClosed pr is closed" }, "title": { "description": "The title of the pull request", @@ -18224,6 +18224,14 @@ "schema": { "type": "integer" } + }, + { + "description": "branch name substring to filter by", + "in": "query", + "name": "q", + "schema": { + "type": "string" + } } ], "responses": { diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl index 9afb61887cf..ab02522c20f 100644 --- a/templates/user/dashboard/feeds.tmpl +++ b/templates/user/dashboard/feeds.tmpl @@ -89,10 +89,10 @@ {{$repo := .Repo}}
    {{range $pushCommit := $push.Commits}} - {{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}} + {{$commitLink := printf "%s/commit/%s" $repoLink $pushCommit.Sha1}}
    - - {{ShortSha .Sha1}} + {{ctx.RenderUtils.AvatarStackPushCommit $pushCommit}} + {{ShortSha $pushCommit.Sha1}} {{ctx.RenderUtils.RenderCommitMessage $pushCommit.Message $repo}} diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl index d5dd64b1a34..62c2df7e8f0 100644 --- a/templates/user/dashboard/milestones.tmpl +++ b/templates/user/dashboard/milestones.tmpl @@ -71,45 +71,45 @@
    -
    +
    {{range .Milestones}} -
  • -
    -

    +
    +
    + {{.Repo.FullName}} {{svg "octicon-milestone" 16}} {{.Name}} -

    -
    - {{.Completeness}}% - + +
    + {{.Completeness}}% +
    -
    -
    -
    +
    +
    +
    {{svg "octicon-issue-opened" 14}} {{ctx.Locale.PrettyNumber .NumOpenIssues}} {{ctx.Locale.Tr "repo.issues.open_title"}}
    -
    +
    {{svg "octicon-check" 14}} {{ctx.Locale.PrettyNumber .NumClosedIssues}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
    {{if .TotalTrackedTime}} -
    +
    {{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Hour}}
    {{end}} {{if .UpdatedUnix}} -
    +
    {{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.milestones.update_ago" (DateUtils.TimeSince .UpdatedUnix)}}
    {{end}} -
    +
    {{if .IsClosed}} {{$closedDate:= DateUtils.TimeSince .ClosedDateUnix}} {{svg "octicon-clock" 14}} @@ -127,22 +127,11 @@ {{end}}
    - {{if and (or $.CanWriteIssues $.CanWritePulls) (not $.Repository.IsArchived)}} - - {{end}}
    {{if .Content}}
    {{.RenderedContent}}
    {{end}} -
  • +
    {{end}} {{template "base/paginate" .}} diff --git a/tests/e2e/issue-project.test.ts b/tests/e2e/issue-project.test.ts index 1595cfadc5a..10466cd822f 100644 --- a/tests/e2e/issue-project.test.ts +++ b/tests/e2e/issue-project.test.ts @@ -372,7 +372,7 @@ test('close project and view in closed projects list', async ({page}) => { await expect(page.locator('.milestone-list')).toContainText(closedProjectTitle); // Close the second project by clicking the close link - const projectCard = page.locator('.milestone-card').filter({hasText: closedProjectTitle}); + const projectCard = page.locator('.milestone-list > .item').filter({hasText: closedProjectTitle}); await projectCard.locator('a.link-action[data-url$="/close"]').click(); // Wait for redirect back to project view page diff --git a/tests/e2e/utils.ts b/tests/e2e/utils.ts index 8dcd6aedae2..5b19793f37b 100644 --- a/tests/e2e/utils.ts +++ b/tests/e2e/utils.ts @@ -159,7 +159,7 @@ export async function createProject( await page.waitForURL(new RegExp(`/${owner}/${repo}/projects$`)); // Extract the project ID from the project link in the list - const projectLink = page.locator('.milestone-list .milestone-card').filter({hasText: title}).locator('a').first(); + const projectLink = page.locator('.milestone-list > .item').filter({hasText: title}).locator('a').first(); const href = await projectLink.getAttribute('href'); const match = /\/projects\/(\d+)/.exec(href || ''); const id = match ? parseInt(match[1]) : 0; diff --git a/tests/integration/actions_approve_test.go b/tests/integration/actions_approve_test.go index 3c9f7a341c0..6d4e4d7f8ac 100644 --- a/tests/integration/actions_approve_test.go +++ b/tests/integration/actions_approve_test.go @@ -139,3 +139,107 @@ jobs: assert.Equal(t, actions_model.StatusWaiting, run2.Status) }) } + +// TestForkPullRequestApprovalNotBypassedByPriorApproval verifies that a single +// approval on a fork PR does not permanently trust the contributor: a subsequent +// fork PR from the same user must still be gated (Blocked / NeedApproval=true) +// until that user has had a pull request merged in the repo. +func TestForkPullRequestApprovalNotBypassedByPriorApproval(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user2Session := loginUser(t, user2.Name) + user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + user4Session := loginUser(t, user4.Name) + user4Token := getTokenForLoggedInUser(t, user4Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + apiBaseRepo := createActionsTestRepo(t, user2Token, "fork-approval-regression", false) + baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID}) + user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(user2APICtx)(t) + + wfTreePath := ".gitea/workflows/ci.yml" + wfContent := `name: CI +on: pull_request +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo ok +` + createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wfTreePath, + getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "add ci", wfContent)) + + // user4 forks the repo + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name), + &api.CreateForkOption{Name: new("fork-approval-regression-fork")}).AddTokenAuth(user4Token) + resp := MakeRequest(t, req, http.StatusAccepted) + apiForkRepo := DecodeJSON(t, resp, &api.Repository{}) + forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiForkRepo.ID}) + user4APICtx := NewAPITestContext(t, user4.Name, forkRepo.Name, auth_model.AccessTokenScopeWriteRepository) + defer doAPIDeleteRepository(user4APICtx)(t) + + // PR #1: a benign change from user4's fork — first-time contributor, gate engages. + doAPICreateFile(user4APICtx, "first.txt", &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + NewBranchName: "first", + Message: "first", + Author: api.Identity{Name: user4.Name, Email: user4.Email}, + Committer: api.Identity{Name: user4.Name, Email: user4.Email}, + Dates: api.CommitDateOptions{Author: time.Now(), Committer: time.Now()}, + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte("first")), + })(t) + pr1, err := doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":first")(t) + assert.NoError(t, err) + + run1 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, TriggerUserID: user4.ID, Ref: fmt.Sprintf("refs/pull/%d/head", pr1.Index)}) + assert.True(t, run1.NeedApproval, "first fork PR must require approval") + assert.Equal(t, actions_model.StatusBlocked, run1.Status) + + // user2 approves run1. + req = NewRequest(t, "POST", fmt.Sprintf("%s/actions/approve-all-checks?commit_id=%s", baseRepo.Link(), pr1.Head.Sha)) + user2Session.MakeRequest(t, req, http.StatusOK) + run1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run1.ID}) + assert.False(t, run1.NeedApproval) + assert.Equal(t, user2.ID, run1.ApprovedBy) + + // PR #2: same user, fresh branch. Pre-fix, this run was created with + // NeedApproval=false and dispatched immediately — the bypass path. + doAPICreateFile(user4APICtx, "second.txt", &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + NewBranchName: "second", + Message: "second", + Author: api.Identity{Name: user4.Name, Email: user4.Email}, + Committer: api.Identity{Name: user4.Name, Email: user4.Email}, + Dates: api.CommitDateOptions{Author: time.Now(), Committer: time.Now()}, + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte("second")), + })(t) + pr2, err := doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":second")(t) + assert.NoError(t, err) + + run2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, TriggerUserID: user4.ID, Ref: fmt.Sprintf("refs/pull/%d/head", pr2.Index)}) + assert.True(t, run2.NeedApproval, "second fork PR must still require approval — prior approval-to-run does not grant trust") + assert.Equal(t, actions_model.StatusBlocked, run2.Status) + assert.EqualValues(t, 0, run2.ApprovedBy) + + // After merging PR #1, user4 becomes a known contributor and the gate lifts for a new PR. + doAPIMergePullRequest(user2APICtx, baseRepo.OwnerName, baseRepo.Name, pr1.Index)(t) + doAPICreateFile(user4APICtx, "third.txt", &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + NewBranchName: "third", + Message: "third", + Author: api.Identity{Name: user4.Name, Email: user4.Email}, + Committer: api.Identity{Name: user4.Name, Email: user4.Email}, + Dates: api.CommitDateOptions{Author: time.Now(), Committer: time.Now()}, + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte("third")), + })(t) + pr3, err := doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":third")(t) + assert.NoError(t, err) + + run3 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, TriggerUserID: user4.ID, Ref: fmt.Sprintf("refs/pull/%d/head", pr3.Index)}) + assert.False(t, run3.NeedApproval, "fork PR from a user with a prior merged PR should not require approval") + }) +} diff --git a/tests/integration/actions_concurrency_test.go b/tests/integration/actions_concurrency_test.go index 47b9112534a..92e6b9ad28b 100644 --- a/tests/integration/actions_concurrency_test.go +++ b/tests/integration/actions_concurrency_test.go @@ -486,14 +486,18 @@ jobs: }, ContentBase64: base64.StdEncoding.EncodeToString([]byte("user4-fix2")), })(t) - doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":do-not-cancel/ccc")(t) - // cannot fetch the task because cancel-in-progress is false + pr3, _ := doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":do-not-cancel/ccc")(t) + // cannot fetch the task: approval still required (user4 has no merged PR) and cancel-in-progress is false runner.fetchNoTask(t) runner.execTask(t, pr2Task1, &mockTaskOutcome{ result: runnerv1.Result_RESULT_SUCCESS, }) pr2Run1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: pr2Run1.ID}) assert.Equal(t, actions_model.StatusSuccess, pr2Run1.Status) + // user2 approves the third PR's run (user4 still has no merged PR, approval still required) + pr3Run1Pending := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, TriggerUserID: user4.ID, Ref: fmt.Sprintf("refs/pull/%d/head", pr3.Index)}) + req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/approve", baseRepo.OwnerName, baseRepo.Name, pr3Run1Pending.ID)) + user2Session.MakeRequest(t, req, http.StatusOK) // fetch the task pr3Task1 := runner.fetchTask(t) _, _, pr3Run1 := getTaskAndJobAndRunByTaskID(t, pr3Task1.Id) diff --git a/tests/integration/actions_route_test.go b/tests/integration/actions_route_test.go index 1591a17ae0a..862656186b6 100644 --- a/tests/integration/actions_route_test.go +++ b/tests/integration/actions_route_test.go @@ -63,18 +63,51 @@ jobs: task2 := runner2.fetchTask(t) _, job2, run2 := getTaskAndJobAndRunByTaskID(t, task2.Id) + require.NoError(t, actions_model.UpsertActionRunJobSummary(t.Context(), repo1.ID, run1.ID, job1.RunAttemptID, job1.ID, 0, "text/markdown", []byte("### Hello summary\n\nFrom first step.\n"))) + require.NoError(t, actions_model.UpsertActionRunJobSummary(t.Context(), repo1.ID, run1.ID, job1.RunAttemptID, job1.ID, 1, "text/markdown", []byte("From second step.\n"))) + // A second job's summary in the same run/attempt: the run view must include it, + // but the single-job view must scope it out. + otherJobID := job1.ID + 1 + require.NoError(t, actions_model.UpsertActionRunJobSummary(t.Context(), repo1.ID, run1.ID, job1.RunAttemptID, otherJobID, 0, "text/markdown", []byte("### Other job summary\n"))) + req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo1.Name, run1.ID)) user2Session.MakeRequest(t, req, http.StatusOK) req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo1.Name, 999999)) user2Session.MakeRequest(t, req, http.StatusNotFound) - // run1 and job1 belong to repo1, success - req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run1.ID, job1.ID)) + findSummary := func(viewResp *actions_web.ViewResponse, jobID int64) *actions_web.ViewJobSummary { + for _, s := range viewResp.State.Run.JobSummaries { + if s.JobID == jobID { + return s + } + } + return nil + } + assertJob1Summary := func(t *testing.T, s *actions_web.ViewJobSummary) { + t.Helper() + require.NotNil(t, s) + assert.Contains(t, string(s.SummaryHTML), "Hello summary") + assert.Contains(t, string(s.SummaryHTML), "From second step") + } + + // Run view: summaries for every job in the run. + req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo1.Name, run1.ID)) resp := user2Session.MakeRequest(t, req, http.StatusOK) viewResp := DecodeJSON(t, resp, &actions_web.ViewResponse{}) + require.Len(t, viewResp.State.Run.JobSummaries, 2) + assertJob1Summary(t, findSummary(viewResp, job1.ID)) + assert.Contains(t, string(findSummary(viewResp, otherJobID).SummaryHTML), "Other job summary") + + // Job view: scoped server-side to the requested job, the other job's summary excluded. + req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run1.ID, job1.ID)) + resp = user2Session.MakeRequest(t, req, http.StatusOK) + viewResp = DecodeJSON(t, resp, &actions_web.ViewResponse{}) assert.Len(t, viewResp.State.Run.Jobs, 1) assert.Equal(t, job1.ID, viewResp.State.Run.Jobs[0].ID) + require.Len(t, viewResp.State.Run.JobSummaries, 1) + assertJob1Summary(t, findSummary(viewResp, job1.ID)) + assert.Nil(t, findSummary(viewResp, otherJobID)) // run2 and job2 do not belong to repo1, failure req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run2.ID, job2.ID)) diff --git a/tests/integration/api_actions_artifact_test.go b/tests/integration/api_actions_artifact_test.go index 9e8444525ff..75b91b61355 100644 --- a/tests/integration/api_actions_artifact_test.go +++ b/tests/integration/api_actions_artifact_test.go @@ -17,10 +17,13 @@ import ( "testing" runnerv1 "gitea.dev/actions-proto-go/runner/v1" + actions_model "gitea.dev/models/actions" auth_model "gitea.dev/models/auth" + "gitea.dev/models/db" repo_model "gitea.dev/models/repo" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" + "gitea.dev/modules/util" "gitea.dev/tests" "github.com/stretchr/testify/assert" @@ -44,6 +47,148 @@ func prepareTestEnvActionsArtifacts(t *testing.T) func() { return f } +func getArtifactFixtureTask(t *testing.T) *actions_model.ActionTask { + t.Helper() + + task, err := actions_model.GetRunningTaskByToken(t.Context(), "8061e833a55f6fc0157c98b883e91fcfeeb1a71a") + require.NoError(t, err) + require.NoError(t, task.LoadJob(t.Context())) + ensureArtifactFixtureTaskSteps(t, task) + return task +} + +func ensureArtifactFixtureTaskSteps(t *testing.T, task *actions_model.ActionTask) { + t.Helper() + + steps, err := actions_model.GetTaskStepsByTaskID(t.Context(), task.ID) + require.NoError(t, err) + + existingIndexes := make(map[int64]bool, len(steps)) + for _, step := range steps { + existingIndexes[step.Index] = true + } + + var stepsToInsert []*actions_model.ActionTaskStep + for _, idx := range []int64{0, 1} { + if existingIndexes[idx] { + continue + } + stepsToInsert = append(stepsToInsert, &actions_model.ActionTaskStep{ + TaskID: task.ID, + Index: idx, + RepoID: task.RepoID, + Status: actions_model.StatusWaiting, + }) + } + if len(stepsToInsert) == 0 { + return + } + + _, err = db.GetEngine(t.Context()).Insert(stepsToInsert) + require.NoError(t, err) +} + +func TestActionsJobSummaryUpload(t *testing.T) { + defer prepareTestEnvActionsArtifacts(t)() + + const runnerToken = "8061e833a55f6fc0157c98b883e91fcfeeb1a71a" + task := getArtifactFixtureTask(t) + summaryURL := func(stepIndex int64) string { + return fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/jobs/%d/steps/%d/summary", task.Job.RunID, task.Job.ID, stepIndex) + } + putSummary := func(stepIndex int64, body, contentType string) *RequestWrapper { + return NewRequestWithBody(t, "PUT", summaryURL(stepIndex), strings.NewReader(body)). + AddTokenAuth(runnerToken). + SetHeader("Content-Type", contentType) + } + + t.Run("success", func(t *testing.T) { + body := "### Uploaded summary\n\n- line one\n" + MakeRequest(t, putSummary(0, body, "text/markdown; charset=utf-8"), http.StatusOK) + + summary, err := actions_model.GetActionRunJobSummary(t.Context(), task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, task.Job.ID, 0) + require.NoError(t, err) + assert.Equal(t, actions_model.JobSummaryContentTypeMarkdown, summary.ContentType) + assert.Equal(t, body, summary.Content) + + staleUpdated := summary.Updated - 60 + _, err = db.GetEngine(t.Context()).ID(summary.ID).Cols("updated").Update(&actions_model.ActionRunJobSummary{Updated: staleUpdated}) + require.NoError(t, err) + + updatedBody := "### Updated summary\n\n- refreshed\n" + MakeRequest(t, putSummary(0, updatedBody, actions_model.JobSummaryContentTypeMarkdown), http.StatusOK) + + summary, err = actions_model.GetActionRunJobSummary(t.Context(), task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, task.Job.ID, 0) + require.NoError(t, err) + assert.Equal(t, updatedBody, summary.Content) + assert.Greater(t, summary.Updated, staleUpdated) + + stepTwoBody := "### Second step summary\n\n- another step\n" + MakeRequest(t, putSummary(1, stepTwoBody, actions_model.JobSummaryContentTypeMarkdown), http.StatusOK) + + summary, err = actions_model.GetActionRunJobSummary(t.Context(), task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, task.Job.ID, 1) + require.NoError(t, err) + assert.Equal(t, stepTwoBody, summary.Content) + + summaries, err := actions_model.ListActionRunJobSummaries(t.Context(), task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, 0) + require.NoError(t, err) + require.Len(t, summaries, 2) + assert.Equal(t, int64(0), summaries[0].StepIndex) + assert.Equal(t, int64(1), summaries[1].StepIndex) + }) + + t.Run("invalid-content-type", func(t *testing.T) { + resp := MakeRequest(t, putSummary(0, "summary", "text/html"), http.StatusBadRequest) + assert.Contains(t, resp.Body.String(), "invalid summary content type") + }) + + t.Run("size-limit", func(t *testing.T) { + resp := MakeRequest(t, putSummary(0, strings.Repeat("a", actions_model.MaxJobSummarySize+1), actions_model.JobSummaryContentTypeMarkdown), http.StatusBadRequest) + assert.Contains(t, resp.Body.String(), "invalid summary") + }) + + t.Run("aggregate-size-limit", func(t *testing.T) { + require.NoError(t, actions_model.UpsertActionRunJobSummary(t.Context(), task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, task.Job.ID, 0, + actions_model.JobSummaryContentTypeMarkdown, []byte(strings.Repeat("a", actions_model.MaxJobSummaryAggregateSize-1024)))) + resp := MakeRequest(t, putSummary(1, strings.Repeat("b", 4096), actions_model.JobSummaryContentTypeMarkdown), http.StatusBadRequest) + assert.Contains(t, resp.Body.String(), "aggregate size exceeded") + }) + + t.Run("job-mismatch", func(t *testing.T) { + req := NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/jobs/%d/steps/0/summary", task.Job.RunID, task.Job.ID+1), strings.NewReader("summary")). + AddTokenAuth(runnerToken). + SetHeader("Content-Type", actions_model.JobSummaryContentTypeMarkdown) + resp := MakeRequest(t, req, http.StatusBadRequest) + assert.Contains(t, resp.Body.String(), "job_id mismatch") + }) + + t.Run("run-mismatch", func(t *testing.T) { + req := NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/jobs/%d/steps/0/summary", task.Job.RunID+1, task.Job.ID), strings.NewReader("summary")). + AddTokenAuth(runnerToken). + SetHeader("Content-Type", actions_model.JobSummaryContentTypeMarkdown) + resp := MakeRequest(t, req, http.StatusBadRequest) + assert.Contains(t, resp.Body.String(), "run-id does not match") + }) + + t.Run("invalid-step-index", func(t *testing.T) { + resp := MakeRequest(t, putSummary(-1, "summary", actions_model.JobSummaryContentTypeMarkdown), http.StatusBadRequest) + assert.Contains(t, resp.Body.String(), "invalid step_index") + }) + + t.Run("step-index-mismatch", func(t *testing.T) { + resp := MakeRequest(t, putSummary(999, "summary", actions_model.JobSummaryContentTypeMarkdown), http.StatusBadRequest) + assert.Contains(t, resp.Body.String(), "step_index mismatch") + }) + + t.Run("empty-body-clears", func(t *testing.T) { + MakeRequest(t, putSummary(0, "### keep me", actions_model.JobSummaryContentTypeMarkdown), http.StatusOK) + MakeRequest(t, putSummary(0, "", actions_model.JobSummaryContentTypeMarkdown), http.StatusOK) + + _, err := actions_model.GetActionRunJobSummary(t.Context(), task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, task.Job.ID, 0) + require.ErrorIs(t, err, util.ErrNotExist) + }) +} + func TestActionsArtifactUploadSingleFile(t *testing.T) { defer prepareTestEnvActionsArtifacts(t)() diff --git a/tests/integration/api_repo_branch_test.go b/tests/integration/api_repo_branch_test.go index 188da619fb9..864e351c8d0 100644 --- a/tests/integration/api_repo_branch_test.go +++ b/tests/integration/api_repo_branch_test.go @@ -128,3 +128,20 @@ func TestAPIRepoBranchesMirror(t *testing.T) { assert.NoError(t, err) assert.JSONEq(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}", string(bs)) } + +func TestAPIRepoBranchesSearch(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteRepository) + + // "test" matches "test_branch" but not "master" + resp := MakeRequest(t, NewRequestf(t, "GET", "/api/v1/repos/org3/repo3/branches?q=test").AddTokenAuth(token), http.StatusOK) + branches := DecodeJSON(t, resp, []api.Branch{}) + assert.Len(t, branches, 1) + assert.Equal(t, "test_branch", branches[0].Name) + + // no match returns empty list + resp = MakeRequest(t, NewRequestf(t, "GET", "/api/v1/repos/org3/repo3/branches?q=doesnotexist").AddTokenAuth(token), http.StatusOK) + branches = DecodeJSON(t, resp, []api.Branch{}) + assert.Empty(t, branches) +} diff --git a/tests/integration/api_user_org_perm_test.go b/tests/integration/api_user_org_perm_test.go index d31abdfeb37..63d69fab717 100644 --- a/tests/integration/api_user_org_perm_test.go +++ b/tests/integration/api_user_org_perm_test.go @@ -151,9 +151,7 @@ func testUnknownOrganization(t *testing.T) { req := NewRequest(t, "GET", "/api/v1/users/user1/orgs/unknown/permissions"). AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusNotFound) - apiError := DecodeJSON(t, resp, &api.APIError{}) - assert.Equal(t, "GetUserByName", apiError.Message) + MakeRequest(t, req, http.StatusNotFound) } func testHiddenMemberPermissionsForbidden(t *testing.T) { diff --git a/tests/integration/auth_oauth2_test.go b/tests/integration/auth_oauth2_test.go index bcd49811494..7978c3bd048 100644 --- a/tests/integration/auth_oauth2_test.go +++ b/tests/integration/auth_oauth2_test.go @@ -13,6 +13,7 @@ import ( "time" auth_model "gitea.dev/models/auth" + "gitea.dev/models/db" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" "gitea.dev/modules/json" @@ -23,6 +24,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "xorm.io/builder" ) // TestMigrateAzureADV2ToOIDC simulates a login source migration from the Azure AD V2 OAuth2 provider to the OpenID Connect provider, @@ -54,7 +56,7 @@ func TestMigrateAzureADV2ToOIDC(t *testing.T) { ) // The fake OIDC server issues tokens containing both sub and oid claims, mirroring what Azure AD v2.0 returns. - srv := newFakeOIDCServer(t, subValue, oidValue) + srv := newFakeOIDCServer(t, FakeOIDCConfig{Sub: subValue, OID: oidValue}) // --- Step 1: Establish the legacy Azure AD V2 state --- // Create an azureadv2 auth source. In production this would have been the source used before the migration. @@ -138,7 +140,7 @@ func TestOIDCIgnoresStaleExternalLoginLinks(t *testing.T) { setup := func(t *testing.T, sourceName, sub, userName, email string) (*auth_model.Source, *user_model.User) { t.Helper() - srv := newFakeOIDCServerWithProfile(t, sub, sub+"-oid", email, "OIDC Test User") + srv := newFakeOIDCServer(t, FakeOIDCConfig{Sub: sub, OID: sub + "-oid", Email: email, Name: "OIDC Test User"}) addOAuth2Source(t, sourceName, oauth2.Source{ Provider: "openidConnect", ClientID: "test-client-id", @@ -191,14 +193,79 @@ func TestOIDCIgnoresStaleExternalLoginLinks(t *testing.T) { }) } -// newFakeOIDCServer starts an httptest.Server that implements the minimum OIDC endpoints needed to complete a sign-in flow: -func newFakeOIDCServer(t *testing.T, sub, oid string) *httptest.Server { - return newFakeOIDCServerWithProfile(t, sub, oid, sub+"@example.com", "OIDC Test User") +// TestOAuth2CallbackReactivationGating exercises the gate in handleOAuth2SignIn: +// an inactive user can only be reactivated when who was disabled by auto-sync +func TestOAuth2CallbackReactivationGating(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() + defer test.MockVariableValue(&setting.OAuth2Client.Username, setting.OAuth2UsernameUserid)() + + srv := newFakeOIDCServer(t, FakeOIDCConfig{Sub: "test-sub", Email: "test@example.com", Name: "Test User"}) + addOAuth2Source(t, "test-oauth-source", oauth2.Source{ + Provider: "openidConnect", + ClientID: "test-client-id", + ClientSecret: "test-client-secret", + OpenIDConnectAutoDiscoveryURL: srv.URL + "/.well-known/openid-configuration", + }) + authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(t.Context(), "test-oauth-source") + require.NoError(t, err) + + u := &user_model.User{Name: "test-user", Email: "test@example.com"} + require.NoError(t, user_model.CreateUser(t.Context(), u, &user_model.Meta{})) + + extLink := &user_model.ExternalLoginUser{ + UserID: u.ID, + LoginSourceID: authSource.ID, + Provider: authSource.Name, + ExternalID: "test-sub", + } + require.NoError(t, user_model.LinkExternalToUser(t.Context(), u, extLink)) + + prepareUserExternalLink := func(t *testing.T, refreshToken string) { + err := user_model.UpdateUserCols(t.Context(), &user_model.User{ID: u.ID, IsActive: false}, "is_active") + require.NoError(t, err) + _, err = db.GetEngine(t.Context()).Where(builder.Eq{"user_id": u.ID}).Cols("refresh_token"). + Update(&user_model.ExternalLoginUser{RefreshToken: refreshToken}) + require.NoError(t, err) + require.False(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: u.ID}).IsActive) + } + + t.Run("admin-disabled user is not reactivated", func(t *testing.T) { + prepareUserExternalLink(t, "non-empty-refresh-token") + doOIDCSignIn(t, authSource.Name) + after := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: u.ID}) + assert.False(t, after.IsActive, "OAuth callback must not re-enable an administrator-disabled account") + }) + + t.Run("auto-sync-disabled user is reactivated", func(t *testing.T) { + prepareUserExternalLink(t, "" /* empty refresh token */) + doOIDCSignIn(t, authSource.Name) + after := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: u.ID}) + assert.True(t, after.IsActive, "OAuth callback must reactivate a sync-disabled account on successful login") + }) } -func newFakeOIDCServerWithProfile(t *testing.T, sub, oid, email, name string) *httptest.Server { +// FakeOIDCConfig holds configuration for the fake OIDC server used in tests. +type FakeOIDCConfig struct { + Sub string + OID string + Email string + Name string + Groups []string +} + +// newFakeOIDCServer starts a httptest.Server that implements the minimum OIDC endpoints needed to complete a sign-in flow +func newFakeOIDCServer(t *testing.T, cfg FakeOIDCConfig) *httptest.Server { t.Helper() + // Set defaults for backward compatibility with existing tests + if cfg.Email == "" { + cfg.Email = cfg.Sub + "@example.com" + } + if cfg.Name == "" { + cfg.Name = "OIDC Test User" + } + var srv *httptest.Server srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -212,11 +279,18 @@ func newFakeOIDCServerWithProfile(t *testing.T, sub, oid, email, name string) *h }) case "/token": // returns an ID token with both "sub" and "oid" claims so tests can verify which one ends up as ExternalID claims := map[string]any{ - "iss": srv.URL, - "aud": "test-client-id", - "exp": time.Now().Add(time.Hour).Unix(), - "sub": sub, - "oid": oid, + "iss": srv.URL, + "aud": "test-client-id", + "exp": time.Now().Add(time.Hour).Unix(), + "sub": cfg.Sub, + "email": cfg.Email, + "name": cfg.Name, + } + if cfg.OID != "" { + claims["oid"] = cfg.OID + } + if cfg.Groups != nil { + claims["groups"] = cfg.Groups } payload, _ := json.Marshal(claims) header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none"}`)) @@ -232,11 +306,18 @@ func newFakeOIDCServerWithProfile(t *testing.T, sub, oid, email, name string) *h }) case "/userinfo": // sub MUST match the id_token sub; goth rejects mismatches. - _ = json.NewEncoder(w).Encode(map[string]any{ - "sub": sub, - "email": email, - "name": name, - }) + response := map[string]any{ + "sub": cfg.Sub, + "email": cfg.Email, + "name": cfg.Name, + } + if cfg.OID != "" { + response["oid"] = cfg.OID + } + if cfg.Groups != nil { + response["groups"] = cfg.Groups + } + _ = json.NewEncoder(w).Encode(response) default: http.NotFound(w, r) } @@ -264,3 +345,147 @@ func doOIDCSignIn(t *testing.T, sourceName string) { callbackURL := fmt.Sprintf("/user/oauth2/%s/callback?code=test-code&state=%s", sourceName, url.QueryEscape(state)) session.MakeRequest(t, NewRequest(t, "GET", callbackURL), http.StatusSeeOther) } + +// newOIDCSource is a helper function to create a configured OAuth2 source for testing +func newOIDCSource(srv *httptest.Server, withAdmin, withRestricted bool) oauth2.Source { + src := oauth2.Source{ + Provider: "openidConnect", + ClientID: "test-client-id", + ClientSecret: "test-client-secret", + OpenIDConnectAutoDiscoveryURL: srv.URL + "/.well-known/openid-configuration", + GroupClaimName: "groups", + } + if withAdmin { + src.AdminGroup = "admins" + } + if withRestricted { + src.RestrictedGroup = "restricted-users" + } + return src +} + +// TestOAuth2GroupClaimsAppliedOnFirstLogin verifies that group claims from OAuth2/OIDC +// are correctly applied to newly created users on the first login +func TestOAuth2GroupClaimsAppliedOnFirstLogin(t *testing.T) { + defer tests.PrepareTestEnv(t)() + // Enable auto-registration to ensure first login creates user with group claims + defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() + // Use sub claim as username for deterministic user naming + defer test.MockVariableValue(&setting.OAuth2Client.Username, setting.OAuth2UsernameUserid)() + + tt := []struct { + Name string + IsAdmin bool + IsRestricted bool + SourceName string + }{ + { + Name: "user in both admin and restricted groups", + IsAdmin: true, + IsRestricted: true, + SourceName: "test-group-claims", + }, + { + Name: "no groups", + IsAdmin: false, + IsRestricted: false, + SourceName: "test-no-groups", + }, + } + for _, tc := range tt { + t.Run(tc.Name, func(t *testing.T) { + // Set up OIDC server with group claims + srv := newFakeOIDCServer(t, FakeOIDCConfig{ + Sub: tc.SourceName, + Email: tc.SourceName + "@example.com", + Name: "Test User", + Groups: []string{"admins", "restricted-users"}, + }) + + // Ensure it's the first login so no user in database + unittest.AssertNotExistsBean(t, &user_model.User{Name: tc.SourceName}) + + addOAuth2Source(t, tc.SourceName, newOIDCSource(srv, tc.IsAdmin, tc.IsRestricted)) + + doOIDCSignIn(t, tc.SourceName) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: tc.SourceName}) + assert.Equal(t, tc.IsAdmin, user.IsAdmin) + assert.Equal(t, tc.IsRestricted, user.IsRestricted) + assert.Equal(t, auth_model.OAuth2, user.LoginType) + }) + } +} + +// TestOAuth2GroupClaimsManualLinking tests that group claims are applied correctly +// when a user goes through the manual linking flow (auto-registration disabled). +func TestOAuth2GroupClaimsManualLinking(t *testing.T) { + defer tests.PrepareTestEnv(t)() + // Disable auto-registration to force manual linking flow + defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, false)() + defer test.MockVariableValue(&setting.Service.AllowOnlyInternalRegistration, false)() + + tt := []struct { + Name string + IsAdmin bool + IsRestricted bool + SourceName string + }{ + { + Name: "user in both admin and restricted groups", + IsAdmin: true, + IsRestricted: true, + SourceName: "test-group-claims-manual-linking", + }, + { + Name: "no groups", + IsAdmin: false, + IsRestricted: false, + SourceName: "test-no-groups-manual-linking", + }, + } + + for _, tc := range tt { + t.Run(tc.Name, func(t *testing.T) { + srv := newFakeOIDCServer(t, FakeOIDCConfig{ + Sub: tc.SourceName, + Email: tc.SourceName + "@example.com", + Name: "Manual User", + Groups: []string{"admins", "restricted-users"}, + }) + addOAuth2Source(t, tc.SourceName, newOIDCSource(srv, tc.IsAdmin, tc.IsRestricted)) + unittest.AssertNotExistsBean(t, &user_model.User{Name: tc.SourceName}) + session := emptyTestSession(t) + resp := session.MakeRequest(t, NewRequest(t, "GET", "/user/oauth2/"+tc.SourceName), http.StatusTemporaryRedirect) + + location := resp.Header().Get("Location") + u, err := url.Parse(location) + require.NoError(t, err) + state := u.Query().Get("state") + require.NotEmpty(t, state, "redirect to OIDC provider must include state") + + callbackURL := fmt.Sprintf("/user/oauth2/%s/callback?code=test-code&state=%s", tc.SourceName, url.QueryEscape(state)) + session.MakeRequest(t, NewRequest(t, "GET", callbackURL), http.StatusSeeOther) + + // Submit the form to create a new account + linkAccountResp := session.MakeRequest(t, NewRequest(t, "GET", "/user/link_account"), http.StatusOK) + // Verify we're on the link account page + assert.Contains(t, linkAccountResp.Body.String(), "link_account") + + // Use NewRequestWithValues to POST form data (no CSRF needed in tests) + // Field names are lowercase in HTML forms: user_name, email, password, retype + req := NewRequestWithValues(t, "POST", "/user/link_account_signup", map[string]string{ + "user_name": tc.SourceName, + "email": tc.SourceName + "@example.com", + "password": "", // AllowOnlyExternalRegistration means no password needed + "retype": "", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: tc.SourceName}) + assert.Equal(t, tc.IsAdmin, user.IsAdmin) + assert.Equal(t, tc.IsRestricted, user.IsRestricted) + assert.Equal(t, auth_model.OAuth2, user.LoginType) + }) + } +} diff --git a/tests/integration/git_helper_for_declarative_test.go b/tests/integration/git_helper_for_declarative_test.go index 3b374a094ee..bd0aedf6c91 100644 --- a/tests/integration/git_helper_for_declarative_test.go +++ b/tests/integration/git_helper_for_declarative_test.go @@ -15,6 +15,7 @@ import ( "testing" "time" + "gitea.dev/modules/generate" "gitea.dev/modules/git" "gitea.dev/modules/git/gitcmd" "gitea.dev/modules/setting" @@ -33,7 +34,7 @@ func withKeyFile(t *testing.T, keyname string, callback func(string)) { assert.NoError(t, err) keyFile := filepath.Join(tmpDir, keyname) - err = ssh.GenKeyPair(keyFile) + err = ssh.GenKeyPair(keyFile, generate.SSHKeyECDSA, 0) assert.NoError(t, err) err = os.WriteFile(filepath.Join(tmpDir, "ssh"), []byte("#!/bin/bash\n"+ diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 9c42366832f..6f0928a12e1 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -323,6 +323,10 @@ func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) { } } +// DecodeJSON decodes then response as JSON into typed variable and return it +// HINT: don't use it on existing variable (reuse existing variable): +// if the existing var already contains some values but the new input doesn't, then it leads to wrong test result in edge cases. +// For slice decoding, use: v := DecodeJSON(t, resp, []T{}) func DecodeJSON[T any](t testing.TB, resp *httptest.ResponseRecorder, v T) (ret T) { t.Helper() diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index aaeba60d8e9..ffb491344b3 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -34,7 +34,7 @@ import ( func getIssuesSelection(t testing.TB, htmlDoc *HTMLDoc) *goquery.Selection { issueList := htmlDoc.doc.Find("#issue-list") assert.Equal(t, 1, issueList.Length()) - return issueList.Find(".item").Find(".issue-item-title") + return issueList.Find(".item").Find(".list-item-large-title") } func getIssue(t *testing.T, repoID int64, issueSelection *goquery.Selection) *issues_model.Issue { diff --git a/tests/integration/repo_commits_test.go b/tests/integration/repo_commits_test.go index ab7119769f8..b55e1372c95 100644 --- a/tests/integration/repo_commits_test.go +++ b/tests/integration/repo_commits_test.go @@ -38,11 +38,15 @@ func TestRepoCommits(t *testing.T) { doc.doc.Find("#commits-table .commit-id-short").Each(func(i int, s *goquery.Selection) { commits = append(commits, path.Base(s.AttrOr("href", ""))) }) - doc.doc.Find("#commits-table .author-wrapper a").Each(func(i int, s *goquery.Selection) { + doc.doc.Find("#commits-table .avatar-stack-names a.muted").Each(func(i int, s *goquery.Selection) { userHrefs = append(userHrefs, s.AttrOr("href", "")) }) assert.Equal(t, []string{"69554a64c1e6030f051e5c3f94bfbd773cd6a324", "27566bd5738fc8b4e3fef3c5e72cce608537bd95", "5099b81332712fe655e34e8dd63574f503f61811"}, commits) - assert.Equal(t, []string{"/user2", "/user21", "/user2"}, userHrefs) + assert.Equal(t, []string{ + "/user2/repo16/commits/branch/master/search?q=author%3Auser2%40example.com", + "/user2/repo16/commits/branch/master/search?q=author%3Auser21%40example.com", + "/user2/repo16/commits/branch/master/search?q=author%3Auser2%40example.com", + }, userHrefs) }) t.Run("LastCommit", func(t *testing.T) { @@ -50,9 +54,9 @@ func TestRepoCommits(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) commitHref := doc.doc.Find(".latest-commit .commit-id-short").AttrOr("href", "") - authorHref := doc.doc.Find(".latest-commit .author-wrapper a").AttrOr("href", "") + authorHref := doc.doc.Find(".latest-commit .avatar-stack-names a").AttrOr("href", "") assert.Equal(t, "/user2/repo16/commit/69554a64c1e6030f051e5c3f94bfbd773cd6a324", commitHref) - assert.Equal(t, "/user2", authorHref) + assert.Equal(t, "/user2/repo16/commits/branch/master/search?q=author%3Auser2%40example.com", authorHref) }) t.Run("CommitListNonExistingCommiter", func(t *testing.T) { @@ -65,17 +69,39 @@ func TestRepoCommits(t *testing.T) { doc := NewHTMLParser(t, resp.Body) commitHref := doc.doc.Find("#commits-table tr:first-child .commit-id-short").AttrOr("href", "") assert.Equal(t, "/user2/repo1/commit/985f0301dba5e7b34be866819cd15ad3d8f508ee", commitHref) - authorElem := doc.doc.Find("#commits-table tr:first-child .author-wrapper") + authorElem := doc.doc.Find("#commits-table tr:first-child .avatar-stack-names") assert.Equal(t, "6543", strings.TrimSpace(authorElem.Text())) }) + t.Run("CommitPageUsesCommitterDate", func(t *testing.T) { + const ( + commitID = "5099b81332712fe655e34e8dd63574f503f61811" + expectedCommitterTime = "2017-08-06T19:56:13+02:00" + authorTime = "2017-08-06T19:55:01+02:00" + ) + + req := NewRequest(t, "GET", "/user2/repo16/commits/branch/master") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + var commitListTime string + doc.doc.Find("#commits-table tbody tr").EachWithBreak(func(_ int, row *goquery.Selection) bool { + if path.Base(row.Find(".commit-id-short").AttrOr("href", "")) != commitID { + return true + } + commitListTime = row.Find("td").Eq(3).Find("relative-time").AttrOr("datetime", "") + return false + }) + require.Equal(t, expectedCommitterTime, commitListTime) + }) + t.Run("LastCommitNonExistingCommiter", func(t *testing.T) { req := NewRequest(t, "GET", "/user2/repo1/src/branch/branch2") resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) commitHref := doc.doc.Find(".latest-commit .commit-id-short").AttrOr("href", "") assert.Equal(t, "/user2/repo1/commit/985f0301dba5e7b34be866819cd15ad3d8f508ee", commitHref) - authorElem := doc.doc.Find(".latest-commit .author-wrapper") + authorElem := doc.doc.Find(".latest-commit .avatar-stack-names") assert.Equal(t, "6543", strings.TrimSpace(authorElem.Text())) }) } diff --git a/tests/integration/wiki_test.go b/tests/integration/wiki_test.go index 0c1b00e5076..11eef67c685 100644 --- a/tests/integration/wiki_test.go +++ b/tests/integration/wiki_test.go @@ -72,7 +72,7 @@ EOF // reader can't push _, _, runErr = gitcmd.NewCommand("push", "origin", "refs/heads/master").WithDir(dstLocalPath).RunStdString(t.Context()) - assert.True(t, gitcmd.StderrContains(runErr, "remote: Repository not found\n")) + assert.Contains(t, runErr.Stderr(), "remote: Repository not found\n") req := NewRequest(t, "GET", "/user2/repo1/wiki/raw/Home.md") resp := MakeRequest(t, req, http.StatusOK) assert.Contains(t, resp.Body.String(), "This is the home page!") diff --git a/tools/migrate-nolyfills.ts b/tools/migrate-nolyfills.ts deleted file mode 100644 index 2e5f635272a..00000000000 --- a/tools/migrate-nolyfills.ts +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env node -// nolyfill writes overrides to package.json#pnpm.overrides which pnpm v11 ignores. -// This moves them to pnpm-workspace.yaml until SukkaW/nolyfill#119 is fixed. -import {readFileSync, writeFileSync} from 'node:fs'; -import {exit} from 'node:process'; -import {fileURLToPath} from 'node:url'; -import {dump} from 'js-yaml'; - -const packagePath = fileURLToPath(new URL('../package.json', import.meta.url)); -const workspacePath = fileURLToPath(new URL('../pnpm-workspace.yaml', import.meta.url)); - -const packageJson: {pnpm?: {overrides?: Record}} = JSON.parse(readFileSync(packagePath, 'utf8')); -const overrides = packageJson.pnpm?.overrides; - -if (!overrides || !Object.keys(overrides).length) { - exit(0); -} - -const block = dump({overrides}, {lineWidth: -1, quotingType: "'"}); -const workspace = readFileSync(workspacePath, 'utf8'); -const overridesRegex = /^overrides:[^\n]*(?:\n(?:[ \t][^\n]*|[ \t]*(?=\n[ \t])))*\n?/m; - -if (!overridesRegex.test(workspace)) { - console.error(`No 'overrides:' block found in pnpm-workspace.yaml`); - exit(1); -} - -writeFileSync(workspacePath, workspace.replace(overridesRegex, block)); - -const pnpm = packageJson.pnpm!; -delete pnpm.overrides; -if (!Object.keys(pnpm).length) delete packageJson.pnpm; -writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`); diff --git a/web_src/css/avatar.css b/web_src/css/avatar.css new file mode 100644 index 00000000000..3b34b8207f4 --- /dev/null +++ b/web_src/css/avatar.css @@ -0,0 +1,125 @@ +img.ui.avatar, +.ui.avatar img, +.ui.avatar svg { + border-radius: var(--border-radius); + object-fit: contain; + aspect-ratio: 1; +} + +.avatar-stack-names { + display: inline-flex; + align-items: center; + align-self: center; + gap: 4px; + white-space: nowrap; + vertical-align: middle; +} + +.avatar-stack-names > a.muted, +.avatar-stack-names > .avatar-stack-popup-trigger { + overflow: hidden; + text-overflow: ellipsis; + max-width: 240px; +} + +/* use semibold for latest commit author */ +.latest-commit .avatar-stack-names > a, +.latest-commit .avatar-stack-names > .avatar-stack-popup-trigger { + font-weight: var(--font-weight-semibold); +} + +/* template emits children reversed; row-reverse re-orders visually and keeps the author last-painted (on top) */ +.avatar-stack { + display: inline-flex; + align-items: center; + flex-direction: row-reverse; +} + +.avatar-stack > * { + margin-left: -16px; + transition: transform 0.15s ease, opacity 0.15s ease; + position: relative; + display: inline-flex; +} + +.avatar-stack > *:last-child { margin-left: 0; } +.avatar-stack > *:nth-last-child(2) { margin-left: -14px; } + +/* hover spreads via transform (no layout shift); positions count from visual-left = last DOM child = :nth-last-child */ +.avatar-stack:hover > *:nth-last-child(2) { transform: translateX(14px); } +.avatar-stack:hover > *:nth-last-child(3) { transform: translateX(30px); } +.avatar-stack:hover > *:nth-last-child(4) { transform: translateX(46px); } +.avatar-stack:hover > *:nth-last-child(5) { transform: translateX(62px); } +.avatar-stack:hover > *:nth-last-child(6) { transform: translateX(78px); } +.avatar-stack:hover > *:nth-last-child(7) { transform: translateX(94px); } +.avatar-stack:hover > *:nth-last-child(8) { transform: translateX(110px); } +.avatar-stack:hover > *:nth-last-child(9) { transform: translateX(126px); } +.avatar-stack:hover > *:nth-last-child(10) { transform: translateX(142px); } +.avatar-stack:hover > *:nth-last-child(11) { transform: translateX(158px); } + +.avatar-stack .avatar { + border: 1px solid var(--color-body); + background: var(--color-body); + transition: border-color 0.15s ease, background-color 0.15s ease; +} + +.avatar-stack:hover .avatar { + background-color: var(--color-body); +} + +.avatar-stack-overflow-chip { + align-items: center; + justify-content: center; + width: 0; + height: 20px; + margin-left: 0; + border: 0 solid var(--color-body); + border-radius: var(--border-radius); + color: var(--color-text); + font-weight: var(--font-weight-semibold); + overflow: hidden; + opacity: 0; + transition: all 0.15s ease; +} + +.avatar-stack:hover .avatar-stack-overflow-chip { + width: 20px; + margin-left: -16px; + border-width: 1px; + opacity: 1; +} + +.avatar-stack-popup-trigger { + cursor: pointer; + background: none; + border: none; + padding: 0; + font: inherit; + color: inherit; +} + +.avatar-stack-popup-trigger:hover { + color: var(--color-primary); +} + +.avatar-stack-popup { + min-width: 200px; + display: flex; + flex-direction: column; + padding: 4px 0; +} + +.avatar-stack-popup > a { + padding: 6px 12px; + gap: 8px; +} + +.avatar-stack-popup > a:hover { + background: var(--color-hover); +} + +@media (max-width: 767.98px) { + .avatar-stack-names { + max-width: 80px; + } +} diff --git a/web_src/css/base.css b/web_src/css/base.css index 49ddca57a30..8b8cfac2d1f 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -386,14 +386,6 @@ a.label, color: var(--color-text-light-2); } -img.ui.avatar, -.ui.avatar img, -.ui.avatar svg { - border-radius: var(--border-radius); - object-fit: contain; - aspect-ratio: 1; -} - .full.height { flex-grow: 1; padding-bottom: var(--page-space-bottom); @@ -454,11 +446,6 @@ img.ui.avatar, margin-right: 4px; } -.ui.inline.delete-button { - padding: 8px 15px; - font-weight: var(--font-weight-normal); -} - .ui .migrate { color: var(--color-text-light-2) !important; } diff --git a/web_src/css/index.css b/web_src/css/index.css index 89ad63ba1a8..6d9280c67f8 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -37,7 +37,6 @@ @import "./modules/charescape.css"; @import "./shared/flex-list.css"; -@import "./shared/milestone.css"; @import "./shared/settings.css"; @import "./features/dropzone.css"; @@ -57,6 +56,7 @@ @import "./font_i18n.css"; @import "./base.css"; +@import "./avatar.css"; @import "./home.css"; @import "./install.css"; diff --git a/web_src/css/modules/dropdown.css b/web_src/css/modules/dropdown.css index 9d3dc48adae..02957fe31d9 100644 --- a/web_src/css/modules/dropdown.css +++ b/web_src/css/modules/dropdown.css @@ -80,6 +80,10 @@ border-top-width: 0; } +.ui.dropdown .menu .hidden { /* for hidden items and dividers */ + display: none; +} + .ui.dropdown .menu > .header { margin: 1rem 0 0.75rem; padding: 0 1.14285714rem; diff --git a/web_src/css/modules/svg.css b/web_src/css/modules/svg.css index 7adfc77a818..b772f98ac6f 100644 --- a/web_src/css/modules/svg.css +++ b/web_src/css/modules/svg.css @@ -2,7 +2,7 @@ and material icons should have no "fill" set explicitly, otherwise some like ".editorconfig" won't render correctly */ .svg:not(.git-entry-icon) { display: inline-block; - vertical-align: text-top; + vertical-align: middle; /* middle is the best choice for different font sizes from 1px to 36px when no flex */ fill: currentcolor; } diff --git a/web_src/css/repo.css b/web_src/css/repo.css index b47e03b8ce4..0682290e1a6 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -1386,8 +1386,7 @@ tbody.commit-list { vertical-align: baseline; } -.message-wrapper, -.author-wrapper { +.message-wrapper { overflow: hidden; text-overflow: ellipsis; max-width: 100%; @@ -1395,12 +1394,6 @@ tbody.commit-list { vertical-align: middle; } -.author-wrapper { - max-width: 180px; - align-self: center; - white-space: nowrap; -} - .latest-commit .message-wrapper { max-width: calc(100% - 2.5rem); } @@ -1415,9 +1408,6 @@ tbody.commit-list { tr.commit-list { width: 100%; } - .author-wrapper { - max-width: 80px; - } } @media (min-width: 768px) and (max-width: 991.98px) { @@ -1816,14 +1806,6 @@ tbody.commit-list { box-shadow: 0 0.5rem 1rem var(--color-shadow) !important; } -.commits-table .commits-table-right form { - display: flex; - align-items: center; - gap: 0.75em; - justify-content: center; - flex-wrap: wrap; -} - @media (max-width: 767.98px) { .repository.view.issue .comment-list, .repository.view.issue .comment-list .timeline-item { @@ -1848,22 +1830,6 @@ tbody.commit-list { flex-basis: auto !important; margin-bottom: 0.5rem !important; } - .commits-table { - flex-direction: column; - } - .commits-table .commits-table-left { - align-items: initial !important; - margin-bottom: 6px; - } - .commits-table .commits-table-right form > div:nth-child(1) { - order: 1; /* the "commit search" input */ - } - .commits-table .commits-table-right form > div:nth-child(2) { - order: 3; /* the "search all" checkbox */ - } - .commits-table .commits-table-right form > button:nth-child(3) { - order: 2; /* the "search" button */ - } .commit-table { overflow-x: auto; } diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css index 1a635f20199..dfffe201874 100644 --- a/web_src/css/repo/issue-list.css +++ b/web_src/css/repo/issue-list.css @@ -34,14 +34,6 @@ } } -#issue-list .issue-item-title { - font-size: 16px; - font-weight: var(--font-weight-semibold); - color: var(--color-text); - text-decoration: none; - overflow-wrap: anywhere; -} - #issue-list .branches { display: inline-flex; } @@ -92,3 +84,47 @@ margin-right: 8px; text-align: left; } + +/* for "title (#123)": + + title ( + #123 + ) + + * hover on "title": also highlight the right parentheses + * hover on "#123": don't highlight other parts + */ +.title-full-link-hover:not(:has(:not(.title-full-link):hover)):hover > a.title-full-link { + color: var(--color-primary); + text-decoration: underline; +} + +.list-item-large-title { + font-size: 16px; + font-weight: var(--font-weight-semibold); + color: var(--color-text); + text-decoration: none; + overflow-wrap: anywhere; +} + +.list-item-title-progress { + width: 200px; + height: 16px; +} + +.list-item-secondary-bar { + display: flex; + flex-wrap: wrap; + gap: var(--gap-block); + justify-content: space-between; + color: var(--color-text-light-2); +} + +.list-item-secondary-bar a { + color: var(--color-text-light-2); + text-decoration: none; +} + +.list-item-secondary-bar a:hover { + color: var(--color-text); +} diff --git a/web_src/css/shared/flex-list.css b/web_src/css/shared/flex-list.css index 1acaa362e22..b224fe667c6 100644 --- a/web_src/css/shared/flex-list.css +++ b/web_src/css/shared/flex-list.css @@ -32,7 +32,7 @@ .items-with-main > .item { display: flex; - gap: 8px; + gap: var(--gap-block); align-items: flex-start; } @@ -44,7 +44,7 @@ .items-with-main > .item .item-main { display: flex; flex-direction: column; - gap: 0.25em; + gap: var(--gap-inline); flex-grow: 1; flex-basis: 60%; /* avoid wrapping the "item-trailing" too aggressively */ min-width: 0; /* make the "text truncate" work, otherwise the flex axis is not limited and the text just overflows */ @@ -52,7 +52,7 @@ .items-with-main > .item .item-header { display: flex; - gap: .25rem; + gap: var(--gap-inline); justify-content: space-between; flex-wrap: wrap; } @@ -63,7 +63,7 @@ .items-with-main > .item .item-trailing { display: flex; - gap: 0.5rem; + gap: var(--gap-block); align-items: center; flex-grow: 0; flex-wrap: wrap; @@ -92,7 +92,7 @@ display: flex; align-items: center; flex-wrap: wrap; - gap: .25rem; + gap: var(--gap-inline); color: var(--color-text-light-2); overflow-wrap: anywhere; } diff --git a/web_src/css/shared/milestone.css b/web_src/css/shared/milestone.css deleted file mode 100644 index 47e822f8d3a..00000000000 --- a/web_src/css/shared/milestone.css +++ /dev/null @@ -1,62 +0,0 @@ -.milestone-list { - list-style: none; -} - -.milestone-card { - width: 100%; - padding-top: 10px; - padding-bottom: 10px; -} - -.milestone-card + .milestone-card { - border-top: 1px solid var(--color-secondary); -} - -.milestone-card .render-content { - padding-top: 10px; -} - -.milestone-header progress { - width: 200px; - height: 16px; -} - -.milestone-header { - display: flex; - align-items: center; - margin: 0; - flex-wrap: wrap; - justify-content: space-between; -} - -.milestone-toolbar { - padding-top: 5px; - display: flex; - flex-wrap: wrap; - gap: 8px; - justify-content: space-between; -} - -.milestone-toolbar .group { - color: var(--color-text-light-2); - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.milestone-toolbar .group > a { - font-size: 15px; - color: var(--color-text-light-2); -} - -.milestone-toolbar .group > a:hover { - color: var(--color-text); -} - -@media (max-width: 767.98px) { - .milestone-card { - display: flex; - flex-direction: column; - gap: 8px; - } -} diff --git a/web_src/fomantic/build/components/dropdown.js b/web_src/fomantic/build/components/dropdown.js index b8f066db748..0faed1858c4 100644 --- a/web_src/fomantic/build/components/dropdown.js +++ b/web_src/fomantic/build/components/dropdown.js @@ -308,12 +308,12 @@ $.fn.dropdown = function(parameters) { firstUnfiltered: function() { module.verbose('Selecting first non-filtered element'); module.remove.selectedItem(); - $item + const $selectable = $item .not(selector.unselectable) - .not(selector.addition + selector.hidden) - .eq(0) - .addClass(className.selected) - ; + .not(selector.addition + selector.hidden); + let $selectedItem = $selectable.filter(`[data-value="${CSS.escape($input.val())}"]`); // GITEA-PATCH: try to re-select the last selected item for single selection + if (!$selectedItem.length) $selectedItem = $item.eq(0); + $selectedItem.addClass(className.selected); }, nextAvailable: function($selected) { $selected = $selected.eq(0); @@ -772,11 +772,13 @@ $.fn.dropdown = function(parameters) { if(!Array.isArray(preSelected)) { preSelected = preSelected && preSelected!=="" ? preSelected.split(settings.delimiter) : []; } - $.each(preSelected,function(index,value){ - $item.filter('[data-value="'+CSS.escape(value)+'"]') // GITEA-PATCH: use "CSS.escape" for query selector + if (module.is.multiple()) { // GITEA-PATCH: only hide selected items when the dropdown is "multiple selection" + $.each(preSelected, function (index, value) { + $item.filter('[data-value="' + CSS.escape(value) + '"]') // GITEA-PATCH: use "CSS.escape" for query selector .addClass(className.filtered) - ; - }); + ; + }); + } afterFiltered(); }); } diff --git a/web_src/js/components/ActionRunJobView.vue b/web_src/js/components/ActionRunJobView.vue index 835518ecb8e..d150e5899b6 100644 --- a/web_src/js/components/ActionRunJobView.vue +++ b/web_src/js/components/ActionRunJobView.vue @@ -2,7 +2,6 @@ import {computed, nextTick, onBeforeUnmount, onMounted, ref, toRefs, watch} from 'vue'; import {SvgIcon} from '../svg.ts'; import ActionStatusIcon from './ActionStatusIcon.vue'; -import WorkflowGraph from './WorkflowGraph.vue'; import {addDelegatedEventListener, createElementFromAttrs, toggleElem} from '../utils/dom.ts'; import {formatDatetime, formatDatetimeISO} from '../utils/time.ts'; import {POST} from '../modules/fetch.ts'; @@ -13,7 +12,6 @@ import {localUserSettings} from '../modules/user-settings.ts'; import type {ActionsArtifact, ActionsJob, ActionsRun, ActionsStatus} from '../modules/gitea-actions.ts'; import { type ActionRunViewStore, - collectCallerChildJobs, createLogLineMessage, type LogLine, type LogLineCommand, @@ -118,14 +116,11 @@ const currentJob = ref({ const stepsContainer = ref(null); const jobStepLogs = ref>([]); -// Reusable workflow caller view: when the selected job is a caller node, the right pane -// shows the children list rather than step logs (callers don't run on a runner). +// Reusable workflow caller view: the right pane shows just the header (name + uses path + +// status). Callers don't run on a runner, and the dependency graph for their children lives +// in the run summary's WorkflowGraph, not here — matching GitHub Actions. const selectedJob = computed(() => (run.value.jobs || []).find((it) => it.id === props.jobId)); const isCallerJob = computed(() => Boolean(selectedJob.value?.isReusableCaller)); -const callerChildJobs = computed(() => { - if (!isCallerJob.value) return []; - return collectCallerChildJobs(run.value.jobs || [], props.jobId); -}); watch(optionAlwaysAutoScroll, () => { saveLocaleStorageOptions(); @@ -477,20 +472,6 @@ async function hashChangeListener() {
    - -
    - -
    -
    @@ -578,8 +559,7 @@ async function hashChangeListener() { border-radius: 3px; } -.job-info-header:has(+ .job-step-container), -.job-info-header:has(+ .caller-children-container) { +.job-info-header:has(+ .job-step-container) { border-radius: var(--border-radius) var(--border-radius) 0 0; } @@ -613,14 +593,6 @@ async function hashChangeListener() { min-width: 0; } -.caller-children-container { - flex: 1; - display: flex; - flex-direction: column; - border-top: 1px solid var(--color-console-border); - color: var(--color-console-fg); -} - .job-step-container { max-height: 100%; border-radius: 0 0 var(--border-radius) var(--border-radius); diff --git a/web_src/js/components/ActionRunSummaryView.vue b/web_src/js/components/ActionRunSummaryView.vue index 17b7e2802e1..e3813e9e174 100644 --- a/web_src/js/components/ActionRunSummaryView.vue +++ b/web_src/js/components/ActionRunSummaryView.vue @@ -1,5 +1,4 @@ + + + + +
    @@ -688,24 +425,34 @@ function onNodeClick(job: JobNode, event: MouseEvent) { display: flex; flex-direction: column; } + .graph-header { display: flex; justify-content: space-between; align-items: center; - padding: 8px 14px; - background: var(--color-box-header); - border-bottom: 1px solid var(--color-secondary); + padding: 16px 16px 8px; + background: var(--color-console-bg); gap: var(--gap-block); flex-wrap: wrap; } -.graph-title { - margin: 0; +.graph-workflow-info { + min-width: 0; +} + +.graph-workflow-name { + display: block; color: var(--color-text); font-size: 16px; font-weight: var(--font-weight-semibold); - flex: 1; - min-width: 200px; + line-height: 1.25; +} + +.graph-workflow-trigger { + margin-top: 4px; + color: var(--color-text-light-2); + font-size: 12px; + line-height: 1.4; } .graph-stats { @@ -715,11 +462,17 @@ function onNodeClick(job: JobNode, event: MouseEvent) { color: var(--color-text-light-1); font-size: 13px; white-space: nowrap; + margin-left: auto; + padding: 0 16px; +} + +.graph-controls { + flex-shrink: 0; } .graph-container { flex: 1; - overflow: hidden; + overflow: auto; padding: 10px 14px 18px; border-radius: 0 0 var(--border-radius) var(--border-radius); cursor: grab; @@ -737,86 +490,210 @@ function onNodeClick(job: JobNode, event: MouseEvent) { } .graph-svg path { - transition: all 0.2s ease; + transition: stroke-width 0.2s ease, opacity 0.2s ease; stroke-linecap: round; stroke-linejoin: round; } +.node-edge { + stroke: var(--color-secondary-dark-2); + stroke-width: 1.5; + opacity: 0.9; +} + .highlighted-edge { - stroke-width: 2 !important; - stroke: var(--color-workflow-edge-hover) !important; + stroke: var(--color-primary); + stroke-width: 2; } .job-node-group { cursor: pointer; - transition: all 0.2s ease; + transition: opacity 0.15s ease; } -.job-node-group:hover .job-rect { - /* due to SVG rendering limitation, only one of fill and drop-shadow can work */ - fill: var(--color-hover); - /* filter: drop-shadow(0 1px 3px var(--color-shadow-opaque)); */ +.job-node-group.caller-node { + cursor: default; } -.job-text-wrap { +.job-node-group:hover .job-rect, +.job-node-group.related-node .job-rect { + stroke: var(--color-primary); + stroke-width: 1.5; + fill: var(--color-primary-alpha-10); +} + +.graph-svg.has-hover .job-node-group:not(.related-node) { + opacity: 0.2; +} + +.graph-svg.has-hover .node-edge:not(.highlighted-edge) { + opacity: 0.15; +} + +.highlighted-edge-layer { + pointer-events: none; +} + +.highlighted-port { + fill: var(--color-primary); + stroke: var(--color-primary); +} + +.job-rect { + fill: var(--color-box-body); + stroke: var(--color-secondary); + stroke-width: 1; +} + +.matrix-foreign-object { + pointer-events: auto; + overflow: visible; +} + +.matrix-panel, +.grouped-panel { + width: 100%; + height: 100%; + box-sizing: border-box; + border-radius: 6px; + background: transparent; + pointer-events: auto; + user-select: none; +} + +.matrix-panel { + display: flex; + flex-direction: column; + padding: 6px 10px 8px; +} + +.matrix-panel-label { + font-size: 10px; + font-weight: var(--font-weight-medium); + color: var(--color-text-light-2); + line-height: 1.3; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; +} + +.matrix-panel-collapsed { + display: flex; + flex-direction: column; + gap: 2px; + padding: 2px 0 0 2px; + cursor: pointer; +} + +.matrix-panel-summary-row { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.matrix-panel-summary { + font-size: 12px; + font-weight: var(--font-weight-semibold); + line-height: 1.3; + color: var(--color-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.matrix-panel-toggle { + font-size: 11px; + color: var(--color-text-light-2); + padding-left: 24px; + cursor: pointer; +} + +.matrix-panel-toggle:hover { + color: var(--color-primary); + text-decoration: underline; +} + +.matrix-panel-jobs { + display: flex; + flex-direction: column; + gap: 2px; + padding: 4px 0 0 2px; + overflow-y: auto; +} + +.grouped-panel { + display: flex; + flex-direction: column; + justify-content: center; + padding: 6px; + gap: 2px; +} + +.graph-list-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + min-height: 24px; + padding: 1px 6px; + border-radius: 5px; +} + +.graph-list-row:hover { + background: var(--color-hover); +} + +.graph-list-row-main, +.job-row-main { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.graph-list-row-name, +.job-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 11px; + font-weight: var(--font-weight-semibold); + color: var(--color-text); +} + +.graph-list-row-duration, +.job-duration { + flex: 0 0 auto; + font-size: 10px; + color: var(--color-text-light-2); + white-space: nowrap; +} + +.job-row { width: 100%; height: 100%; display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: center; - gap: 1px; - padding: 4px 8px 4px 0; - overflow: hidden; -} - -.job-name { - width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 12px; - font-weight: var(--font-weight-semibold); - color: var(--color-text); - user-select: none; - pointer-events: none; -} - -.job-duration { - font-size: 10px; - line-height: 1.2; - color: var(--color-text-light-2); - white-space: nowrap; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - user-select: none; - pointer-events: none; -} - -.job-status-fg-obj, -.job-status-icon-wrap { - pointer-events: none; -} - -.job-status-icon-wrap { - width: 20px; - height: 20px; - display: flex; align-items: center; - justify-content: center; + justify-content: space-between; + gap: 8px; +} + +.job-card { + border-radius: 6px; + padding: 0 2px; } .node-port { - fill: var(--color-box-body); - stroke: var(--color-light-border); + fill: var(--color-secondary-dark-2); + stroke: var(--color-box-body); stroke-width: 1.25; - opacity: 0.85; + opacity: 0.9; pointer-events: none; } -.node-edge { - transition: stroke-width 0.2s ease, opacity 0.2s ease; - opacity: 0.75; +.job-node-group.related-node .node-port { + fill: var(--color-primary); } diff --git a/web_src/js/features/repo-actions.ts b/web_src/js/features/repo-actions.ts index 1b994992481..3d4bd49a4f3 100644 --- a/web_src/js/features/repo-actions.ts +++ b/web_src/js/features/repo-actions.ts @@ -27,9 +27,14 @@ export function initRepositoryActionView() { pushedBy: el.getAttribute('data-locale-runs-pushed-by'), summary: el.getAttribute('data-locale-summary'), allJobs: el.getAttribute('data-locale-all-jobs'), + jobSummaries: el.getAttribute('data-locale-job-summaries'), expandCallerJobs: el.getAttribute('data-locale-expand-caller-jobs'), collapseCallerJobs: el.getAttribute('data-locale-collapse-caller-jobs'), triggeredVia: el.getAttribute('data-locale-triggered-via'), + rerunTriggered: el.getAttribute('data-locale-rerun-triggered'), + backToPullRequest: el.getAttribute('data-locale-back-to-pull-request'), + backToWorkflow: el.getAttribute('data-locale-back-to-workflow'), + statusLabel: el.getAttribute('data-locale-status-label'), totalDuration: el.getAttribute('data-locale-total-duration'), artifactsTitle: el.getAttribute('data-locale-artifacts-title'), artifactExpired: el.getAttribute('data-locale-artifact-expired'), diff --git a/web_src/js/features/repo-commit.ts b/web_src/js/features/repo-commit.ts index 16f38993749..6103766202a 100644 --- a/web_src/js/features/repo-commit.ts +++ b/web_src/js/features/repo-commit.ts @@ -24,3 +24,29 @@ export function initCommitStatuses() { }); }); } + +export function initAvatarStackPopup() { + registerGlobalInitFunc('initAvatarStackPopup', (el: HTMLElement) => { + const nextEl = el.nextElementSibling!; + if (!nextEl.matches('.tippy-target')) throw new Error('Expected next element to be a tippy target'); + createTippy(el, { + content: nextEl, + placement: 'bottom-start', + interactive: true, + role: 'dialog', + theme: 'menu', + trigger: 'click', + hideOnClick: true, + }); + }); +} + +export function initCommitFileHistoryFollowRename() { + registerGlobalInitFunc('initCommitHistoryFollowRename', (el: HTMLInputElement) => { + el.addEventListener('change', () => { + const url = new URL(window.location.toString()); + url.searchParams.set('follow-rename', `${el.checked}`); + window.location.assign(url.toString()); + }); + }); +} diff --git a/web_src/js/features/repo-new.ts b/web_src/js/features/repo-new.ts index b690ace0f7f..8b1188f19f1 100644 --- a/web_src/js/features/repo-new.ts +++ b/web_src/js/features/repo-new.ts @@ -47,7 +47,6 @@ function initRepoNewTemplateSearch(form: HTMLFormElement) { value: String(tmplRepo.repository.id), }); } - $repoTemplateDropdown.fomanticExt.onResponseKeepSelectedItem($repoTemplateDropdown, inputRepoTemplate.value); return {results}; }, cache: false, diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 094caefb7e0..6f081f30202 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -21,7 +21,7 @@ import {initMarkupContent} from './markup/content.ts'; import {initRepoFileView} from './features/file-view.ts'; import {initUserExternalLogins, initUserCheckAppUrl} from './features/user-auth.ts'; import {initRepoPullRequestReview, initRepoIssueFilterItemLabel} from './features/repo-issue.ts'; -import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; +import {initRepoEllipsisButton, initCommitStatuses, initAvatarStackPopup, initCommitFileHistoryFollowRename} from './features/repo-commit.ts'; import {initRepoTopicBar} from './features/repo-home.ts'; import {initAdminCommon} from './features/admin/common.ts'; import {initRepoCodeView} from './features/repo-code.ts'; @@ -123,6 +123,7 @@ const initPerformanceTracer = callInitFunctions([ initRepoCodeView, initBranchSelectorTabs, initRepoEllipsisButton, + initCommitFileHistoryFollowRename, initRepoDiffCommitBranchesAndTags, initRepoEditor, initRepoGraphGit, @@ -145,6 +146,7 @@ const initPerformanceTracer = callInitFunctions([ initRepoRecentCommits, initCommitStatuses, + initAvatarStackPopup, initCaptcha, initUserCheckAppUrl, diff --git a/web_src/js/modules/fomantic/dropdown.test.ts b/web_src/js/modules/fomantic/dropdown.test.ts index dd3497c8fce..542cb854b84 100644 --- a/web_src/js/modules/fomantic/dropdown.test.ts +++ b/web_src/js/modules/fomantic/dropdown.test.ts @@ -13,13 +13,13 @@ test('hideScopedEmptyDividers-simple', () => {
    `); hideScopedEmptyDividers(container); expect(container.innerHTML).toEqual(` - +
    a
    - - + +
    b
    - + `); }); @@ -35,7 +35,7 @@ test('hideScopedEmptyDividers-items-all-filtered', () => { hideScopedEmptyDividers(container); expect(container.innerHTML).toEqual(`
    - +
    a
    b
    @@ -52,7 +52,7 @@ test('hideScopedEmptyDividers-hide-last', () => { hideScopedEmptyDividers(container); expect(container.innerHTML).toEqual(`
    a
    - +
    b
    `); }); @@ -68,9 +68,9 @@ test('hideScopedEmptyDividers-scoped-items', () => { hideScopedEmptyDividers(container); expect(container.innerHTML).toEqual(`
    a
    - +
    b
    - +
    c
    `); }); diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index e49c410682f..a12b56d23d9 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -8,7 +8,6 @@ const fomanticDropdownFn = $.fn.dropdown; export function initAriaDropdownPatch() { if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once'); $.fn.dropdown = ariaDropdownFn; - $.fn.fomanticExt.onResponseKeepSelectedItem = onResponseKeepSelectedItem; $.fn.fomanticExt.onDropdownAfterFiltered = onDropdownAfterFiltered; (ariaDropdownFn as FomanticInitFunction).settings = fomanticDropdownFn.settings; } @@ -296,8 +295,8 @@ export function hideScopedEmptyDividers(container: Element) { let curScope: string = '', lastVisibleScope: string = ''; const isDivider = (item: Element) => item.classList.contains('divider'); const isScopedDivider = (item: Element) => isDivider(item) && item.hasAttribute('data-scope'); - const hideDivider = (item: Element) => item.classList.add('hidden', 'transition'); // dropdown has its own classes to hide items - const showDivider = (item: Element) => item.classList.remove('hidden', 'transition'); + const hideDivider = (item: Element) => item.classList.add('hidden'); // dropdown has its own classes to hide items + const showDivider = (item: Element) => item.classList.remove('hidden'); const isHidden = (item: Element) => item.classList.contains('hidden') || item.classList.contains('filtered') || item.classList.contains('tw-hidden'); const handleScopeSwitch = (itemScope: string) => { if (curScopeVisibleItems.length === 1 && isScopedDivider(curScopeVisibleItems[0])) { @@ -347,19 +346,3 @@ export function hideScopedEmptyDividers(container: Element) { if (visibleItems[i + 1].matches('.divider')) hideDivider(visibleItems[i]); } } - -function onResponseKeepSelectedItem(dropdown: typeof $ | HTMLElement, selectedValue: string) { - // There is a bug in fomantic dropdown when using "apiSettings" to fetch data - // * when there is a selected item, the dropdown insists on hiding the selected one from the list: - // * in the "filter" function: ('[data-value="'+value+'"]').addClass(className.filtered) - // - // When user selects one item, and click the dropdown again, - // then the dropdown only shows other items and will select another (wrong) one. - // It can't be easily fix by using setTimeout(patch, 0) in `onResponse` because the `onResponse` is called before another `setTimeout(..., timeLeft)` - // Fortunately, the "timeLeft" is controlled by "loadingDuration" which is always zero at the moment, so we can use `setTimeout(..., 10)` - const elDropdown = (dropdown instanceof HTMLElement) ? dropdown : (dropdown as any)[0]; - setTimeout(() => { - queryElems(elDropdown, `.menu .item[data-value="${CSS.escape(selectedValue)}"].filtered`, (el) => el.classList.remove('filtered')); - $(elDropdown).dropdown('set selected', selectedValue ?? ''); - }, 10); -} diff --git a/web_src/js/modules/gitea-actions.ts b/web_src/js/modules/gitea-actions.ts index 005138a4d4d..bdaac6ef81c 100644 --- a/web_src/js/modules/gitea-actions.ts +++ b/web_src/js/modules/gitea-actions.ts @@ -23,7 +23,12 @@ export type ActionsRun = { duration: string, triggeredAt: number, triggerEvent: string, + pullRequest?: { + index: string, + link: string, + } | null, jobs: Array, + jobSummaries?: Array, commit: { localeCommit: string, localePushedBy: string, @@ -32,6 +37,7 @@ export type ActionsRun = { pusher: { displayName: string, link: string, + avatarLink: string, }, branch: { name: string, @@ -41,6 +47,12 @@ export type ActionsRun = { }, }; +export type ActionsJobSummary = { + jobId: number, + jobName: string, + summaryHTML: string, +}; + export type ActionsRunAttempt = { attempt: number; status: ActionsStatus; @@ -51,6 +63,7 @@ export type ActionsRunAttempt = { triggeredAt: number; triggerUserName: string; triggerUserLink: string; + triggerUserAvatar: string; }; export type ActionsJob = { diff --git a/web_src/js/svg.ts b/web_src/js/svg.ts index 23c3e7ee015..4d86930de0a 100644 --- a/web_src/js/svg.ts +++ b/web_src/js/svg.ts @@ -7,6 +7,7 @@ import giteaEmptyCheckbox from '../../public/assets/img/svg/gitea-empty-checkbox import giteaExclamation from '../../public/assets/img/svg/gitea-exclamation.svg'; import giteaRunning from '../../public/assets/img/svg/gitea-running.svg'; import octiconArchive from '../../public/assets/img/svg/octicon-archive.svg'; +import octiconArrowLeft from '../../public/assets/img/svg/octicon-arrow-left.svg'; import octiconArrowSwitch from '../../public/assets/img/svg/octicon-arrow-switch.svg'; import octiconBlocked from '../../public/assets/img/svg/octicon-blocked.svg'; import octiconBold from '../../public/assets/img/svg/octicon-bold.svg'; @@ -94,6 +95,7 @@ const svgs = { 'gitea-exclamation': giteaExclamation, 'gitea-running': giteaRunning, 'octicon-archive': octiconArchive, + 'octicon-arrow-left': octiconArrowLeft, 'octicon-arrow-switch': octiconArrowSwitch, 'octicon-blocked': octiconBlocked, 'octicon-bold': octiconBold,